usehooks-ts
usehooks-ts copied to clipboard
[Suggestion] useFetch, delayed for non-GET
The code below is a bit customized for my needs, but it should be easy to follow... The point of this change to for non-get methods to have a delayed fetch call... so I implemented a setBody state that does the setting of the body and the fetch after the hook is instantiated. I'd PR but don't have the time to.
Usage (get):
const { data, error } = useFetch('/spots', 'GET', true);
useEffect(() => {
console.log('data', data);
console.log('error', error);
}, [data, error]);
Usage (post):
const { data, error, setBody } = useFetch('/features', 'POST', true);
useEffect(() => {
console.log('data', data);
console.log('error', JSON.stringify(error));
}, [data, error]);
...
// Do the fetch, in a callback
setBody({
name: 'Testing',
});
Code (JS in my case):
/*
Based on https://usehooks-ts.com/react-hook/use-fetch
*/
/* eslint-disable import/no-cycle */
import Constants from 'expo-constants';
import { useContext, useEffect, useReducer, useRef, useState } from 'react';
import { FirebaseContext } from '../firebase';
const SERVER_URL = Constants.manifest?.extra?.SERVER_URL;
// interface State<T> {
// data?: T
// error?: Error
// }
// type Cache<T> = { [url: string]: T }
// discriminated union type
// type Action<T> =
// | { type: 'loading' }
// | { type: 'fetched'; payload: T }
// | { type: 'error'; payload: Error }
export function useFetch(relativeUrl, method, authenticate = false) {
const cache = useRef({});
const { authToken } = useContext(FirebaseContext);
const [body, setBody] = useState();
const url = `${SERVER_URL}${relativeUrl}`;
// Used to prevent state update if the component is unmounted
const cancelRequest = useRef(false);
const initialState = {
error: undefined,
data: undefined,
setBody: (body) => {
console.log('useFetch:setBody', body);
setBody(body);
},
};
// Keep state logic separated
const fetchReducer = (state, action) => {
switch (action.type) {
case 'loading':
return { ...initialState };
case 'fetched':
return { ...initialState, data: action.payload };
case 'error':
return { ...initialState, error: action.payload };
default:
return state;
}
};
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
dispatch({ type: 'loading' });
}, []);
useEffect(() => {
// Do nothing if the url is not given
if (!url || state.data || state.error) return;
cancelRequest.current = false;
const fetchData = async () => {
if (!url || state.data || state.error) return;
dispatch({ type: 'loading' });
// If a cache exists for this url, return it
if (cache.current[url]) {
dispatch({ type: 'fetched', payload: cache.current[url] });
return;
}
try {
const options = {
method,
headers: authenticate
? {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
}
: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
};
console.log('useFetch:options', options);
const response = await fetch(url, options);
if (!response.ok) {
response
.json()
.then((error) => {
console.log('Error:', error);
throw new Error(error);
})
.catch((error) => {
console.log('Error:', error);
throw new Error(error);
});
throw new Error(response.statusText);
}
const data = await response.json();
cache.current[url] = data;
if (cancelRequest.current) return;
dispatch({ type: 'fetched', payload: data });
} catch (error) {
if (cancelRequest.current) return;
dispatch({ type: 'error', payload: error });
}
};
if (method === 'GET' || body !== undefined) {
console.log('fetchData...');
fetchData();
}
// Use the cleanup function for avoiding a possibly...
// ...state update after the component was unmounted
return () => {
cancelRequest.current = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, body]);
return state;
}