I've built plenty of React Custom Hooks before but only recently built my first useFetch
custom hook. The code is base on me reading lots of articles on the subject and then implementing my own version. It generally works well except that sporadically the cleanup function is fired when it shouldn't be, which means that the expected server data is not fetched. Disabling the cleanup function appears to fix this but clearly a cleanup function is required for when a fetch is made and then the user leave the component for another, etc.
Here's the code, with the "troublesome" line of code commented out near the bottom:
import * as React from 'react';
import axios from 'axios';
import isEmpty from 'lodash/isEmpty';
import { DataDictionary, RestAction } from 'utils/globalTypes';
import { convertObjectToSnakeCase } from 'utils/utils';
/**
* This React Hook provides an easy way to make asynchronous calls via Axios.
* When the `autoFetch` prop is included and `true` then fetching starts immediately.
* Otherwise the `beginFetching` callback function must be included when destructuring and
* explicitly called from the the component that is consuming this hook.
*
* Upon instantiation, two different destructuring signatures can be used:
* - const [data, error, isFetching] = useFetch(...);
* - const [data, error, isFetching, beginFetching] = useFetch(...);
*
* Obviously, if `autoFetch` is not included or `false` then `beginFetching` must be used!
*
* @param {RestAction} action
* @param {string} uri
* @param {boolean} [autoFetch]
* @param {DataDictionary} [body]
*
* @returns [ data, isFetching, error ]
*/
export const useFetch = (
action: RestAction,
uri: string,
autoFetch?: boolean,
body?: DataDictionary
) => {
const isAutoFetch = autoFetch ?? false;
const [data, setData] = React.useState<any>(null);
const [isFetching, setIsFetching] = React.useState(false);
const [error, setError] = React.useState<any>(null);
React.useEffect(() => {
setData(null);
setError(null);
if (isAutoFetch) {
setIsFetching(true);
}
}, [action, uri, isAutoFetch]);
React.useEffect(() => {
/**
* We will not allow the fetch to proceed if any of the following are true:
* - isFetching is true
* - It's a POST or PATCH action and the `body` prop is empty
*/
if (!isFetching || ([RestAction.POST, RestAction.PATCH].includes(action) && isEmpty(body))) {
return;
}
const controller = new AbortController();
const fetchData = async () => {
try {
let response = null;
switch(action) {
case RestAction.GET:
response = await axios.get(uri, {signal: controller.signal});
break;
case RestAction.POST:
if (body) {
response = await axios.post(uri, convertObjectToSnakeCase(body), {signal: controller.signal});
}
break;
case RestAction.PATCH:
if (body) {
response = await axios.patch(uri, convertObjectToSnakeCase(body), {signal: controller.signal});
}
break;
case RestAction.DELETE:
response = await axios.delete(uri, {signal: controller.signal});
break;
default:
break;
}
setData(response?.data);
} catch (error) {
setError(error);
console.error(error);
} finally {
setIsFetching(false)
}
};
if (!isEmpty(uri)) {
fetchData();
}
// TODO: This cleanup function sporadically causes problems, essentially
// aborting the request when we don't want it to. Not sure why it does this.
// Removing this line seems to fix things but obviously we need it!
// return () => controller.abort(); // Cleanup function
}, [isFetching, action, uri, body]);
/**
* If `autoFetch` is not included or is `false` then the async call must be explicitly started by the
* parent component calling this function. We'll first clear the previous data, and possibly error,
* and then proceed with a new async call.
*/
const beginFetching = () => {
setData(null);
setError(null);
setIsFetching(true);
};
return [ data, error, isFetching, beginFetching ] as const;
};
Any suggestions would be appreciated!