I've got two custom react hooks one is just a wrapper for the other one.
The first one is a basic fetch hook, which returns the status and result or error.
interface State<T> {
status: 'idle' | 'pending' | 'error' | 'success';
data?: T;
error?: string;
}
interface Cache<T> {
[url: string]: T;
}
type Action<T> =
| { type: 'request' }
| { type: 'success'; payload: T }
| { type: 'failure'; payload: string };
export function useFetch<T = unknown>(
url: string,
options?: RequestInit,
cached = false
): State<T> {
const cache = useRef<Cache<T>>({});
const cancelRequest = useRef(false);
const initialState: State<T> = {
status: 'idle',
error: undefined,
data: undefined,
};
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'request':
return { ...initialState, status: 'pending' };
case 'success':
return { ...initialState, status: 'success', data: action.payload };
case 'failure':
return { ...initialState, status: 'error', error: action.payload };
default:
return state;
}
};
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'request' });
if (cache.current[url] && cached) {
dispatch({ type: 'success', payload: cache.current[url] });
} else {
try {
const res = await fetch(url, options);
const json = await res.json();
cache.current[url] = json;
if (cancelRequest.current) return;
if (res.status !== 200) {
dispatch({ type: 'failure', payload: json.error });
} else {
dispatch({ type: 'success', payload: json });
}
} catch (error) {
if (cancelRequest.current) return;
dispatch({ type: 'failure', payload: error.message });
}
}
};
fetchData();
return () => {
cancelRequest.current = true;
};
}, [url, options, cached]);
return state;
}
export default useFetch;
The second hook is a wrapper which provides some fetch options like a header containing a cookie to the useFetch hook. This way I wont have to write the same code over and over to do some requests against my API. I've called it useAPICall
export const useAPICall = <T>(url: string, body?: any, method = 'GET') => {
return useFetch<T>(
apiEndpoint + url,
{
method,
body,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'x-access-token': getCookie('token'),
},
},
false
);
};
getCookie returns the cookie called token from the document.
This is how I would use the useAPICall hook:
const { status, data, error } = useAPICall<Request>('/example/route');
My problem is that whenever I want to use the useAPICall or the useFetch hook, while providing an options object as argument, it causes infinite rerenders and a maximum update depth exceeded error.
I figured out the problem lies within the options dependency of useEffect in my useFetch hook, when I remove it from the dependency array the fetch request works nicely.
The only problem being that my linter tells me to include options in the dependency array because useEffect relies on it.
I'm now wondering how I can do it correctly.