← Back to React Course | Chapter 6: Custom Hooks & Advanced Patterns | Lesson 2 of 10

Building a useFetch Hook

A useFetch hook is like a helper that goes and gets information from the internet for you, then hands you the result along with a note about whether it's still loading.

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

markup
<!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

markup
<!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

markup
<!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>
Common Mistakes
  1. Forgetting to handle the loading state, so the UI shows nothing (or stale data) while the request is in flight.
  2. Not cancelling the fetch when the component unmounts, which can cause a 'state update on unmounted component' warning.
  3. Re-running the fetch on every render because the URL or dependency array wasn't stabilized correctly.
Chapter Summary
  • 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.
Browser Support

Available since React 16.8 (Hooks introduction); relies on the standard fetch() API, supported in all modern evergreen browsers.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.