While I don’t feel like coding React without hooks, react-cache still seems to be still far away. Surely, caching in data fetching important, nevertheless I would like to seek possibilities of implementations only with React Hooks. These might be obsoleted in the future, but I want something today, that is “useFetch”. This could be still useful without react-cache in case sophisticated caching is not necessary.
I’ve started developing “useFetch” as a tutorial to build a custom hook.
Firstly, there’s some utility functions, and two notable ones:
const useForceUpdate = () => useReducer(state => !state, false)[1];This is a custom hook to force re-rendering.
const createPromiseResolver = () => { let resolve; const promise = new Promise((r) => { resolve = r; }); return { resolve, promise }; };This is a naive way to create promise that can be resolved from outside.
Next is the main part of the hook:
export const useFetch = (input, opts = defaultOpts) => { const forceUpdate = useForceUpdate(); const error = useRef(null); const loading = useRef(false); const data = useRef(null); const promiseResolver = useMemo(createPromiseResolver, [input, opts]);We define some variables with useRef, and promiseResolver with useMemo.
// ...continued useEffect(() => { let finished = false; const abortController = new AbortController(); (async () => { if (!input) return; // start fetching loading.current = true; forceUpdate(); const onFinish = (e, d) => { if (!finished) { finished = true; error.current = e; data.current = d; loading.current = false; } }; try { const { readBody = defaultReadBody, ...init } = opts; const response = await fetch(input, { ...init, signal: abortController.signal, }); // check response if (response.ok) { const body = await readBody(response); onFinish(null, body); } else { onFinish(createFetchError(response), null); } } catch (e) { onFinish(e, null); } // finish fetching promiseResolver.resolve(); })(); const cleanup = () => { if (!finished) { finished = true; abortController.abort(); } error.current = null; loading.current = false; data.current = null; }; return cleanup; }, [input, opts]);Hmm, too long. Some notes: a) forceUpdate() is called when variables are updated. b) At the end of async function, primiseResolver.resolve() is called. c) the cleanup is to abort running fetch and clear variables. d) the input array of useEffect is [input, opts].
// ...continued if (loading.current) throw promiseResolver.promise; return { error: error.current, data: data.current, }; };Finally, it returns some of the variables, but before that if it’s still in the loading state, it throws a promise so that Suspense catches it and show a fallback loading component.
Let me show the minimal example.
import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import { useFetch } from 'react-hooks-fetch'; const Err = ({ error }) => <span>Error:{error.message}</span>; const DisplayRemoteData = () => { const url = 'https://jsonplaceholder.typicode.com/posts/1'; const { error, data } = useFetch(url); if (error) return <Err error={error} />; if (!data) return null; return <span>RemoteData:{data.title}</span>; }; const App = () => ( <Suspense fallback={<span>Loading...</span>}> <DisplayRemoteData /> </Suspense> ); ReactDOM.render(<App />, document.getElementById('app'));The component that uses useFetch does not handle the loading state and Suspense outside of the component does it and shows loading state. One important note is the line if (!data) return null; which is required because it’s null at first. This is not very nice at the moment, and we would like to find out if there’s a workaround.
You can try this example in codesandbox.
There are some other examples you can find in the GitHub repo with the link to codesandbox.
At first, I was hoping to use both Suspense and ErrorBoundary to show the loading state and the error state so that the main component with useFetch only cares about successful case. It turns out that it involves more or less hacks, and I ended up with normal error variable return by a hook. There’s also a note in the :
Suspense is also hard to work with AbortController. At the point of writing it’s pretty much like a hack. For those who are interested, the code is . I really want to find a better way for this. I’m curious how react-cache handle aborting. Until then, we continue to improve it.
I Love PHP