Reputation: 18410
My useFetch custom hook:
import React, { useState, useEffect } from 'react';
const useFetch = (url, method = 'get') => {
const [response, setResponse] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
setLoading(true);
const res = await fetch(url, {
signal: controller.signal,
method: method.toUpperCase()
});
const json = await res.json();
setResponse(json);
setLoading(false);
console.log('data fetched!!!', url);
} catch (error) {
if (error.name === 'AbortError') {
setError('Aborted!');
} else {
setError(error);
}
setLoading(false);
}
})();
return () => {
controller.abort();
};
}, [url]);
return { response, loading, error };
};
export default useFetch;
Using this hook inside the following component hierarchy: Catalog > Pagination.
Contents of Catalog component, where do i use a useFetch
hook:
const Catalog = () => {
const [limit] = useState(12);
const [offset, setOffset] = useState(0);
const { response, loading, error } = useFetch(
`http://localhost:3000/products?${new URLSearchParams({
limit,
offset
})}`
);
// onPageChange fired from Pagination component, whenever i click on any of the page numbers.
const onPageChange = index => {
setOffset(index);
};
return (
<Pagination
items={response.data}
count={response.count}
pageSize={limit}
onPageChange={onPageChange}
/>
);
}
Then, a Pagination component:
const Pagination = ({ items, count, pageSize, onPageChange }) => {
useEffect(() => {
if (items && items.length) {
setPage(1)();
}
}, []);
const setPage = page => e => {
if (e) e.preventDefault();
let innerPager = pager;
if (page < 1 || page > pager.totalPages) {
return null;
}
innerPager = getPager(count, page, pageSize);
setPager(innerPager);
onPageChange(innerPager.startIndex); // fires a function from the Catalog component
};
const getPager = (totalItems, currentPage, pageSize) => {
currentPage = currentPage || 1;
pageSize = pageSize || 10;
const totalPages = Math.ceil(totalItems / pageSize);
let startPage, endPage;
if (totalPages <= 10) {
startPage = 1;
endPage = totalPages;
} else {
if (currentPage <= 6) {
startPage = 1;
endPage = totalPages;
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9;
endPage = totalPages;
} else {
startPage = currentPage - 5;
endPage = currentPage + 4;
}
}
let startIndex = (currentPage - 1) * pageSize;
let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
const pages = [...Array(endPage + 1 - startPage).keys()].map(
i => startPage + i
);
return {
totalItems,
currentPage,
pageSize,
totalPages,
startPage,
endPage,
startIndex,
endIndex,
pages
};
};
return (
<a className="page-link" href="#" onClick={e => setPage(page)(e)}>{page}</a>
); // shortened markup, i'm mapping paginator buttons from a getPager() function, each button has onClick event listener, which fires a setPage()()
}
A problem i currently experience now, is when i click on a pagination button, e.g: 1, 2, 3, my useFetch
hook fires a first time with a correct offset value, for example: if i click on a 2nd page, offset would be equal to 12, if i click on a 3rd page, offset would be 24 and so on.
But in my case a useFetch
hook is fired twice, first time with correct offset data, then a 2nd time, with initial offset data = 0. I'm only watching for the url changes inside useFetch
hook, so my offset is changed just once, when i press on a pagination button.
Upvotes: 0
Views: 2196
Reputation: 4445
Based on the conversation I had with OP, the main issue was the useEffect
inside of Pagination
component that would get triggered every time the component mounted. Even though the useEffect
used an empty dependency array, which would prevent calling the useEffect
hook once again when new props
got passed, the Pagination
component was getting unmounted because of the logic inside Catalog
.
// it more or less looked like this
const Catalog = () => {
const {response, loading, error} = useFetch(...)
const onPageChange = (index) => {...}
if (loading) { return `Loading` }
return <Pagination {...props} />
}
When loading
was set back to true after calling onPageChange
from the Pagination
component, the useEffect
hook inside of that same component would get called with a constant, setPage(1)()
and reset the newly fetch items to the default offset
, which is 1
in this case.
The if (loading)
statement would unmount the Pagination
component when it evaluated to true
, causing the useEffect
to run again inside Pagination
because it was mounting for the "first time."
const Pagination = (props) => {
useEffect(() => {
if (...) setPage(1)()
// empty array will prevent running this hook again
// if props change and component re-renders,
// but DOES NOT get unmounted and mounted again
}, [])
}
The OP needs to figure out the current page number inside of the Pagination
component, hence the useEffect
that called setPage(1)()
. My suggestion was to move that business logic to the useFetch
hook instead and figure out the current page number based on the response from the API.
I tried to come up with a solution and created a new hook that was more explicit, called useFetchPosts
, that utilizes the useFetch
hook, but returns the page
, and pageCount
along with other relevant data from the API.
function useFetchPosts({ limit = 12, offset = 0 }) {
const response = useFetch(
`https://example.com/posts?${new URLSearchParams({ limit, offset })}`
);
const [payload, setPayload] = React.useState(null);
React.useEffect(() => {
if (!response.data) return;
const { data, count } = response.data;
const lastItem = [...data].pop();
const page = Math.floor(lastItem.id / limit);
const pageCount = Math.ceil(count / limit);
setPayload({
data,
count,
page,
pageCount
});
}, [response, limit, offset]);
return {
response: payload,
loading: response.isFetching,
error: response.error
};
}
So now, instead of calculating what the current page is inside of Pagination
, we can get rid of the useEffect
hook that was causing issues and just simply pass that information to the Pagination
component from Catalog
.
function Catalog() {
const [limit] = React.useState(12);
const [offset, setOffset] = React.useState(1);
const { response, loading, error } = useFetchPosts({
limit,
offset
});
// ...
return (
<Pagination
items={response.data}
count={response.count}
pageSize={limit}
pageCount={response.pageCount}
page={response.page}
onPageChange={onPageChange}
/>
);
}
This can be further optimized and perhaps done a little better, but here's an example that does what it's supposed to when different pages are clicked:
Hope this helps.
Upvotes: 1