Building a useFetch Hook
In this page:
Why Wrap fetch() in a Hook
Fetching data typically needs three pieces of state: the data itself, whether it's still loading, and whether an error occurred. Writing this trio manually in every component that needs data is repetitive. A useFetch hook bundles all three concerns into one reusable function.
Note: Any time you write the same 'const [data, setData] = useState(); const [loading, setLoading] = useState(true);' pattern twice, extract it into a hook.
Warning: A generic useFetch is fine for simple cases, but for real apps a dedicated library like React Query handles caching and retries far more robustly.
Example: Why Wrap fetch() in a Hook
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
setData({ message: "Loaded from " + url });
setLoading(false);
}, [url]);
return { data, loading };
}
function App() {
const { data, loading } = useFetch("/api/greeting");
return <p>{loading ? "Loading..." : data.message}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Handling Loading and Error States
A robust useFetch also tracks an error state, so the component can show a friendly message instead of crashing when a request fails. The hook sets loading back to false and stores the error whether the fetch succeeds or fails.
Note: Always reset both loading and error at the start of a new fetch, in case the URL changes and a previous error is still lingering.
Warning: Forgetting to catch fetch() rejections means a failed network request produces an unhandled promise rejection instead of a visible error state.
Example: Handling Loading and Error States
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useFetch(shouldFail) {
const [error, setError] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
if (shouldFail) setError("Network error");
setLoading(false);
}, [shouldFail]);
return { error, loading };
}
function App() {
const { error, loading } = useFetch(true);
if (loading) return <p>Loading...</p>;
return <p style={{color: "red"}}>{error ? "Error: " + error : "Success"}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Avoiding State Updates After Unmount
If a component unmounts before its fetch finishes, calling setState afterward triggers a React warning and can mask a memory leak. A cleanup flag inside useEffect's return function prevents this by skipping the state update if the component is gone.
Note: Use a simple boolean flag (like cancelled) rather than trying to abort the fetch itself, unless you specifically need AbortController.
Warning: Without this cleanup check, rapidly switching between pages that use useFetch can produce console warnings about updating unmounted components.
Example: Avoiding State Updates After Unmount
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useSafeFetch(value) {
const [result, setResult] = React.useState(null);
React.useEffect(() => {
let cancelled = false;
setTimeout(() => { if (!cancelled) setResult(value); }, 0);
return () => { cancelled = true; };
}, [value]);
return result;
}
function App() {
const result = useSafeFetch("Safe data");
return <p>{result || "Loading..."}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Forgetting to handle the loading state, so the UI shows nothing (or stale data) while the request is in flight.
- Not cancelling the fetch when the component unmounts, which can cause a 'state update on unmounted component' warning.
- Re-running the fetch on every render because the URL or dependency array wasn't stabilized correctly.
- useFetch is a custom hook pattern that wraps fetch() plus loading/error state into one reusable hook.
- It returns data, loading, and error so any component can render the right UI for each state.
- The fetch runs inside useEffect, with the URL in the dependency array so it re-fetches when the URL changes.
- A cleanup flag prevents state updates after the component has unmounted.
Available since React 16.8 (Hooks introduction); relies on the standard fetch() API, supported in all modern evergreen browsers.
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: