Pagination works from the second click - javascript

I have such a problem with pagination: it switches to another page only from the second click. When I click on page 2, it also remains on page 1, and only from the second time it switches to page 2. Also with the rest of the pages.
I did pagination component like this:
const Paginator = ({
total,
startPage = 1,
limit = 2,
totalPages = null,
onMovePage = null,
}) => {
const [hovered, setHovered] = useState(false);
const handleEnter = () => {
setHovered(true);
};
const handleLeave = () => {
setHovered(false);
};
const style = hovered ? { left: "-230px" } : {};
const [currentPage, setCurrentPage] = useState(startPage);
function range(start, stop, step) {
if(typeof stop=='undefined'){/*one param defined*/stop=start;start=0}
if(typeof step=='undefined'){step=1}
if((step>0&&start>=stop)||(step<0&&start<=stop)){return[]}
let result=[];
for(let i=start;step>0?i<stop:i>stop;i+=step){result.push(i)}
return result;
};
return (
<>
...
{range(1, totalPages+1).map(p => (
<PagItem key={p} handleClick={ () => {setCurrentPage(p); onMovePage && onMovePage({currentPage})} } title={p} name={p} />
))}
...
</>
}
And using it in softwares component:
const PER_PAGE = 2;
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const onFetchData = ({ currentPage }) => {
console.log('currentPage in onFetchData', currentPage)
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}&per_page=${PER_PAGE}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
})
}
useEffect(() => {
onFetchData({ currentPage: 1 })
}, []);
return (
<>
...
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator totalPages={totalPages} total={total} onMovePage={onFetchData} limit={PER_PAGE} />
...
</>
);
};
So why is it happening?

Change the below
<PagItem key={p} handleClick={ () => {setCurrentPage(p); onMovePage && onMovePage({currentPage})} } title={p} name={p} />
to
<PagItem key={p} handleClick={ () => {setCurrentPage(p); onMovePage && onMovePage({currentPage:p})} } title={p} name={p} />
Because you're assuming your state currentPage is set by the time you call onMovePage which isn't true. Rely on the p to move to that page instead of state which will be set asynchronously.

Related

React useEffect causing function to run 7 times. I am using a useCallback but it still runs to many times

The updateAddedState function with the console.log("running") is running 7 times on a page refresh/initial render.
I only want the updateAddedState function to run once when the addedItems state updates.
I only what the useEffect to run when the actual addedItems state has changed. What am I doing wrong??
export const DropdownMultiSelect = ({
data,
placeholder,
updateState,
}: IProps) => {
const [searchTerm, setSearchTerm] = useState<string>("");
const [filteredData, setFilteredData] = useState<IData[]>(data);
const [addedItems, setAddedItems] = useState<IData[]>([]);
const [placeholderValue, setPlaceholderValue] = useState<string>("");
const [inputActive, setInputActive] = useState<boolean>(false);
const onFocus = () => setInputActive(true);
const onBlur = () => {
setInputActive(false);
};
const updateAddedState = useCallback(() => {
console.log("running");
updateState(addedItems);
}, [updateState, addedItems]);
const handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};
const handleFilterData = useCallback(
(searchTerm: string) => {
let newFilter = data.filter((value) => {
return value.name.toLowerCase().includes(searchTerm.toLowerCase());
});
for (let i = 0; i < addedItems.length; i++) {
for (let j = 0; j < newFilter.length; j++) {
if (addedItems[i].id === newFilter[j].id) {
newFilter.splice(j, 1);
}
}
}
setFilteredData(newFilter);
},
[addedItems, data]
);
const addItem = (value: IData) => {
setAddedItems([...addedItems, value]);
setSearchTerm("");
handleFilterData("");
setInputActive(false);
};
const removeItem = (value: IData, e: React.MouseEvent) => {
e.preventDefault();
let newArray: IData[] = [];
for (let i = 0; i < addedItems.length; i++) {
newArray.push(addedItems[i]);
}
for (let i = 0; i < newArray.length; i++) {
if (value.id === newArray[i].id) {
newArray.splice(i, 1);
}
}
setAddedItems(newArray);
setInputActive(true);
};
useEffect(() => {
if (addedItems.length === 1) {
setPlaceholderValue(`${addedItems.length} vald`);
} else if (addedItems.length > 1) {
setPlaceholderValue(`${addedItems.length} valda`);
} else {
setPlaceholderValue(placeholder);
}
}, [addedItems, placeholderValue, placeholder]);
useEffect(() => {
handleFilterData(searchTerm);
}, [searchTerm, addedItems, handleFilterData]);
useEffect(() => {
let isMounted = true;
if (isMounted) {
if (addedItems) {
updateAddedState();
}
}
return () => {
isMounted = false;
};
}, [updateAddedState, addedItems]);
return (
<div id="dropdownMulti">
<section className="inputSection">
<input
type="text"
placeholder={placeholderValue}
className="inputSection__input"
onChange={handleFilter}
value={searchTerm}
onFocus={onFocus}
onBlur={onBlur}
/>
<div className="inputSection__icon-container">
{inputActive ? (
<AiOutlineUpCircle
onClick={() => setInputActive(false)}
className="inputSection__icon-container--up"
/>
) : (
<AiOutlineDownCircle className="inputSection__icon-container--down" />
)}
</div>
</section>
<section className="addedItems-section">
{inputActive &&
addedItems.map((addedItem) => {
return (
<div className="addedItem" key={addedItem.id}>
<p className="addedItem__item">{addedItem?.name}</p>
<button
data-testid="remove-btn"
className="addedItem__button"
onMouseDown={(e: React.MouseEvent) =>
removeItem(addedItem, e)
}
>
<AiOutlineCloseCircle />
</button>
</div>
);
})}
</section>
{inputActive && (
<ul className="dataResult">
{filteredData.slice(0, 10).map((value) => {
return (
<li
className="dataResult__item"
key={value.id}
tabIndex={0}
onMouseDown={() => addItem(value)}
>
{value.name}
</li>
);
})}
</ul>
)}
</div>
);
};
Any tips on how to cut the number of times it runs?
Try to remove React Strict Mode, it makes components render twice only in development, not in production. Put back on if it's the case.
Try removing updateState function from the dependency array of the useCallback function.
const updateAddedState = useCallback(() => {
console.log("running");
updateState(addedItems);
}, [addedItems]);
If it's still not working define the updateState function which comes with the props with the useCallback hook where the function is defined.

Not rerender not modified items when using useCallback function

A page list items, 10 by 10, with an infinite scroll.
Each item has a button "add to favorite", which when is pressed called the callback function handleClickFavorite, in order to not rerender items already rendered.
But, when handleClickFavorite is called, "data" are not fresh... If I had "data" dependency to handleClickFavorite = useCallback(async (item) => {...}, [user, data]);, "data" will be fresh, but each time I load more items, all items are rerended (I have a console.log into my Card PureComponent). So, how How can I do to use a fresh "data" in my handleClickFavorite without rerendered all my items please ?
const Home = () => {
const [user, setUser] = useState({ email: null, auth: false, favorites: [] });
const [data, setData] = useState([]);
const [isInfiniteDisabled, setInfiniteDisabled] = useState(false);
const config = useRef({
page: 0,
});
const loadData = (ev) => {
setInfiniteDisabled(true);
config.current.page += 1;
service.findAll(config.current.page).then(res => {
setData([
...data,
...res['items']
]);
});
}
useIonViewWillEnter(() => {
console.log('useIonViewWillEnter');
loadData();
});
const handleClickFavorite = useCallback(async (item) => {
if (user.auth) {
user.favorites.push(item.id);
setUser(user);
const datas = [...data];
for (let k in datas) {
if (datas[k].id === item.id) {
datas[k].rated = !datas[k].rated;
}
}
setData(datas);
} else {
// show login modal
}
}, [user]);
return (
<IonPage>
<IonContent fullscreen>
<IonList>
{data.map((item, index) => {
return <Card key={'card' + item.id} details={item} onClickFavorite={handleClickFavorite} />
})}
</IonList>
<IonInfiniteScroll
onIonInfinite={loadData}
threshold="100px"
disabled={isInfiniteDisabled}
>
<IonInfiniteScrollContent
loadingSpinner="bubbles"
loadingText="Loading more data..."
></IonInfiniteScrollContent>
</IonInfiniteScroll>
</IonContent>
</IonPage>
);
};

Component re-renders self after API call whilst using useRef

I'm working on a project and wanted to try and implement an infinitely scrollable page to list users. Filtering and such works fine and all but every time the scrolling component reaches the ref element the component makes an API call to append to the list and then the parent component re-renders self completely.
const UsersList = () => {
const [searchString, setSearchString] = useState('')
const [next, setNext] = useState('')
const { userList, error, nextPage, loading, hasMore } = useFetch(next)
const [usersList, setUsersList] = useState([])
const observer = useRef()
const lastElemRef = useCallback(
(node) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setNext((prev) => (prev = nextPage))
setUsersList((prev) => new Set([...prev, ...userList]))
}
})
if (node) {
observer.current.observe(node)
}
},
[loading, nextPage, hasMore],
)
useEffect(() => {
setUsersList((prev) => new Set([...prev, ...userList]))
console.log(error)
}, [])
return (
<>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<CContainer
className="w-100 justify-content-center"
style={{ maxWidth: 'inherit', overflowY: 'auto', height: 600 }}
>
<CFormInput
className="mt-2 "
id="userSearchInput"
value={searchString}
onChange={(e) => {
setSearchString(e.target.value)
}}
/>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<>
{Array.from(usersList)
.filter((f) => f.username.includes(searchString) || searchString === '')
.map((user) => {
if (Array.from(usersList)[usersList.size - 1] === user) {
return (
<UserCard
key={user.id}
user={user}
parentRef={searchString ? null : lastElemRef}
/>
)
} else {
return <UserCard key={user.id} user={user} />
}
})}
</>
)}
</CContainer>
)}
</>
)
}
export default UsersList
This is the component entirely.
Here's my useFetch hook;
export const useFetch = (next) => {
const [userList, setUserList] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [nextPage, setNextPage] = useState(next)
const [hasMore, setHasMore] = useState(true)
useEffect(() => {
setLoading(true)
setError('')
axios
.get(next !== '' ? `${next}` : 'http://localhost:8000/api/getUsers/', {
headers: {
Authorization: 'Bearer ' + localStorage.getItem('access_token'),
},
})
.then((res) => {
setUserList((userList) => new Set([...userList, ...res.data.results]))
setNextPage((prev) => (prev = res.data.next))
if (res.data.next === null) setHasMore(false)
setLoading(false)
})
.catch((err) => {
setError(err)
})
}, [next])
return { userList, error, nextPage, loading, hasMore }
}
export default useFetch
I'm using Limit Offset Pagination provided by Django Rest Framework, next object just points to the next set of objects to fetch parameters include ?limit and ?offset added at the end of base API url. What is it that I'm doing wrong here ? I've tried many different solutions and nothing seems to work.
Solved
Apparently it was just my back-end not cooperating with my front-end so I've changed up the pagination type and now it seems to behave it self.

How to deal with code that depends on document.title in Next.js with SSG?

I have 2 dynamic SSG pages under /blog/[slug], inside of these pages I am rendering a component with next/link, I can click those links to go to another slug, the problem is that I want to run some code that depends on document.title, I tried a combination of possible solutions:
const ref = createRef<HTMLDivElement>()
useEffect(() => {
while (ref.current?.firstChild) {
ref.current.firstChild.remove()
}
const timeout = setTimeout(() => {
if (typeof window === "object") {
const scriptElement = document.createElement("script")
scriptElement.src = "https://utteranc.es/client.js"
scriptElement.async = true
scriptElement.defer = true
scriptElement.setAttribute("crossorigin", "annonymous")
scriptElement.setAttribute("repo", "my/repo")
scriptElement.setAttribute("issue-term", "title")
scriptElement.setAttribute("theme", "photon-dark")
ref.current?.appendChild(scriptElement)
}
}, 0)
return () => {
clearTimeout(timeout)
}
}, [])
...
return <div ref={ref} />
The problem is that useEffect does not run when switching between pages, this code only works when I visit refresh my page, how can I work with this code when navigating between pages to make it work using a up to date document title?
Edit:
const BlogPost = ({
recordMap,
post,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
if (!post) {
return null
}
const [script, setScript] = useState<HTMLScriptElement | null>(null)
const ref = createRef<HTMLDivElement>()
const router = useRouter()
useEffect(() => {
const handleRouteChange = () => {
const scriptElement = document.createElement("script")
scriptElement.src = "https://utteranc.es/client.js"
scriptElement.async = true
scriptElement.defer = true
scriptElement.setAttribute("crossorigin", "annonymous")
scriptElement.setAttribute("repo", "daniellwdb/website")
scriptElement.setAttribute("issue-term", "title")
scriptElement.setAttribute("theme", "photon-dark")
setScript(scriptElement)
}
router.events.on("routeChangeComplete", handleRouteChange)
return () => {
router.events.off("routeChangeComplete", handleRouteChange)
}
}, [])
useEffect(() => {
if (script) {
ref.current?.appendChild(script)
setScript(null)
} else {
ref.current?.firstChild?.remove()
}
}, [script])
return (
<>
<Box as="main">
<Container maxW="2xl" mb={16}>
<Button as={NextChakraLink} href="/" variant="link" my={8}>
🏠 Back to home page
</Button>
<NotionRenderer
className="notion-title-center"
recordMap={recordMap}
components={{
// Bit of a hack to add our own component where "NotionRenderer"
// would usually display a collection row.
// eslint-disable-next-line react/display-name
collectionRow: () => <BlogPostHero post={post} />,
code: Code,
equation: Equation,
}}
fullPage
darkMode
/>
<Pagination pagination={pagination ?? {}} />
<Box mt={4} ref={ref} />
<Footer />
</Container>
</Box>
</>
)
}
You can listen to the router.events:
useEffect(() => {
const handleRouteChange = (url, { shallow }) => {
//...
}
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [])

How to display posts on the current page from the API

I'm getting data from Django Rest API and React for Frontend, and I need to create the pagination with posts. I did it all in pagination component. I created the state with current page and I'm changing it by clicking on the page button in component like this:
const Paginator = () => {
const [count, setCount] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(null);
const [nextPage, setNextPage] = useState(null);
const [previousPage, setPreviousPage] = useState(null);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setCount(data.count);
setTotalPages(data.total_pages)
setNextPage(data.links.next);
setPreviousPage(data.links.previous);
setValid(true);
})
}, [currentPage]);
...
return (
<>
{
...
<PbStart style={style} totalPages={range(1, totalPages+1)} setCurrentPage={setCurrentPage} />
...
}
</>
);
};
const PagItem = ({key, handleClick, className, title, name }) => {
return (
<li key={key} onClick={handleClick}>
<Link to='/' className={className} title={`Go to page ${title}`}>
{name}
</Link>
</li>
);
};
const PbStart = ({ style, totalPages, setCurrentPage }) => {
return (
...
{totalPages.map(p => (
<PagItem key={p} handleClick={() => setCurrentPage(p)} title={p} name={p} />
))}
...
);
};
And in posts component I don't know how to change current page, or getting it from the paginaton component. I've written that like this:
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}, [currentPage]);
return (
<>
{
...
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
...
}
</>
);
};
So, how to do that(get the current page from pagination component or another way)?
I think a Paginator's job is only moving between pages and updating current page state. It should not be fetching data by itself, you can provide functionality to do extra work with props.
I haven't tested this, but this might be a good starting point.
With the example below you'll have a list of articles and then below it next and previous buttons.
In Softwares, as you can see I am passing the same function for handling next and previous pages, you can refactor it to have one function like onPageMove and call this function handleNext and handlePrev.
I added two separate functions if you have want to handle something different in either.
const Paginator = ({
total, // Required: Total records
startPage = 1, // Start from page / initialize current page to
limit = 30, // items per page
onMoveNext = null, // function to call next page,
onMovePrev = null, // function to call previous page
}) => {
const [currentPage, setCurrentPage] = useState(startPage);
const canGoNext = total >= limit;
const canGoPrev = currentPage > 1;
function handleNext(e) {
if (canGoNext) {
setCurrentPage((prevState) => prevState+1);
onMoveNext && onMoveNext({ currentPage });
}
}
function handlePrev(e) {
if (canGoPrev) {
setCurrentPage((prevState) => prevState-1);
onMovePrev && onMovePrev({ currentPage });
}
}
return (
<div>
<button onClick={handlePrev} disabled={!canGoPrev}>Prev</button>
<button onClick={handleNext} disabled={!canGoNext}>Next</button>
</div>
);
};
Here is how you can use Paginator in other components.
const PER_PAGE = 30; // get max # of records per page
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [valid, setValid] = useState(false);
const onFetchData = ({ currentPage }) => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}&per_page=${PER_PAGE}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}
useEffect(() => {
onFetchData({ currentPage: 1 })
}, []);
return (
<>
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator total={softwares.length} limit={PER_PAGE} onMoveNext={onFetchData} onMovePrev={onFetchData} />
</>
);
};

Categories