React Mastery / Custom Hooks

useFetch Custom Hook

Creating a custom useFetch hook is a rite of passage in React. It’s all about taking that repetitive useEffect and fetch logic and wrapping it into a neat, reusable package.

Here is your step-by-step guide to building a robust version.


1. The Basic Structure

First, we define the hook. A custom hook is just a JavaScript function whose name starts with "use". We’ll need three pieces of state: the data, the loading status, and any potential errors.

import { useState, useEffect } from 'react';

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  // Logic goes here...

  return { data, isLoading, error };
};

2. Implementing the Fetch Logic

We use useEffect to trigger the fetch whenever the url changes.

Pro Tip: Always check if the response is "ok" before parsing. Fetch doesn't throw an error for 404 or 500 status codes by default; it only throws for network failures.

useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw Error('Could not fetch the data for that resource');
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, [url]); // Dependency array ensures this runs when url changes

3. Handling Unmounted Components (Cleanup)

If a user clicks away from a page while a fetch is still in progress, React might try to update the state of a component that no longer exists (a "memory leak"). We use an AbortController to cancel the request if the component unmounts.

The Finalized Hook:

import { useState, useEffect } from 'react';

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortCont = new AbortController();

    fetch(url, { signal: abortCont.signal })
      .then(res => {
        if (!res.ok) throw Error('Fetch failed');
        return res.json();
      })
      .then(data => {
        setData(data);
        setIsLoading(false);
        setError(null);
      })
      .catch(err => {
        if (err.name === 'AbortError') {
          console.log('fetch aborted');
        } else {
          setIsLoading(false);
          setError(err.message);
        }
      });

    return () => abortCont.abort(); // Cleanup function
  }, [url]);

  return { data, isLoading, error };
};

export default useFetch;

4. How to Use It

Now you can use this hook in any component with just one line of code.

Feature

Benefit

Reusability

Use it for any API endpoint.

Clean Code

Removes 20+ lines of logic from your UI components.

Consistency

Loading and error states are handled the same way everywhere.

Example Usage:

const { data: blogs, isLoading, error } = useFetch('<https://jsonplaceholder.typicode.com/posts>');

return (
  <div>
    { error && <div>{ error }</div> }
    { isLoading && <div>Loading...</div> }
    { blogs && <BlogList blogs={blogs} /> }
  </div>
);