I'm trying to use a path param which I fetch from the URL path as an ID for an entity which I'm trying to fetch. I've created a custom data fetching hook which triggers when either the path of the passed params change. I'm use useParams from react router dom to get the ID of the book from the URL.
Below is the component code:
const BookDetails: FC<BookDetailsProps> = () => {
let { bookId } = useParams();
const { response, loading, error } = useApi(`/books/v1/volumes/${bookId}`);
return <Wrapper></Wrapper>;
};
export default BookDetails;
And below is my custom hook:
const useApi = (url: string, params = {}) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await api(url, { params: params });
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}, [url, params]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { response, error, loading, fetchData };
};
export default useApi;
Now when I use my API hook with a component which fetches data using the params object everything is fine. The component doesn't rerender and I just get the data, but for some reason when I use it with path params it goes off.
Does anyone know how I should proceed?
CodePudding user response:
params
gets a new object instance on each call so the dependency array in the useCallback
will never be the same between different calls (you haven't pass a params
argument to your useApi
) and therefore the useEffect
will be triggered on each render casing a re-render, hence the infinite rerendering
CodePudding user response:
I think the issue is that when no params
object is passed to the useApi
hook
useApi(`/books/v1/volumes/${bookId}`)
The params
argument is initialized to a default empty object value.
const useApi = (url: string, params = {}) => {
...
This causes the params
variable to be a new object reference anytime the component rerenders for any reason and will retrigger the useEffect
hook because fetchData
will be a newly recomputed reference.
...
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await api(url, { params: params });
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}, [url, params]); // <-- params new reference
useEffect(() => {
fetchData();
}, [fetchData]); // <-- fetchData becomes new reference
...
};
I suggest not initializing param
and only provide a fallback value when calling the api
function.
Example:
const useApi = (url: string, params) => { // <-- don't initialize
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await api(url, { params: params || {} }); // <-- provide fallback value
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
}, [url, params]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { response, error, loading, fetchData };
};
export default useApi;
This way params
will be an undefined value from render to render and not change shallow reference equality each render.