TypeOfNaN

Writing Your Own useFetch Hook in React

Nick Scialli
January 04, 2021

React Hooks have been all the rage for a little over a year. Let’s see how we can roll our own useFetch hook to abstract fetch request logic out of our components.

Note: This is for academic purposes only. You could roll your own useFetch hook and use it in production, but I would highly recommend using an established library like use-http to do the heavy lifting for you!

Our useFetch Function Signature

To determine our useFetch function signature, we should consider the information we might need from the end user to actually execute our fetch request. In this case, we’ll say that we need the resource url and we need the options that might go along with the request (e.g., request method).

function useFetch(initialUrl, initialOptions) {
  // Hook here
}

In a more full-featured solution, we might give the user a way ot abort the request, but we’re happy with our two arguments for now!

Maintaining State in Our Hook

Our hook is going to need to maintain some state. We will at least need to maintain url and options in state (as we’ll need to give our user a way to setUrl and setOptions). There are some other stateful variable’s we’ll want as well!

  • data (the data returned from our request)
  • error (any error if our request fails)
  • loading (a boolean indicating whether we are actively fetching)

Let’s create a bunch of stateful variables using the built-in useState hook. also, we’re going to want to give our users the chance to do the following things:

  • set the url
  • set options
  • see the retrieved data
  • see any errors
  • see the loading status

Therefore, we must make sure to return those two state setting functions and three data from our hook!

import { useState } from 'React';

function useFetch(initialUrl, initialOptions) {
  const [url, setUrl] = useState(initialUrl);
  const [options, setOptions] = useState(initialOptions);
  const [data, setData] = useState();
  const [error, setError] = useState();
  const [loading, setLoading] = useState(false);

  // Some magic happens here

  return { data, error, loading, setUrl, setOptions };
}

Importantly, we default our url and options to the initialUrl and initialOptions provided when the hook is first called. Also, you might be thinking that these are a lot of different variables and you’d like to maintain them all in the same object, or a few objects—and that would be totally fine!

Running an Effect When our URL or Options Change

This is a pretty important part! We are going to want to execute a fetch request every time the url or options variables change. What better way to do that than the built-in useEffect hook?

import { useState } from 'React';

function useFetch(initialUrl, initialOptions) {
  const [url, setUrl] = useState(initialUrl);
  const [options, setOptions] = useState(initialOptions);
  const [data, setData] = useState();
  const [error, setError] = useState();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Fetch here
  }, [url, options]);

  return { data, error, loading, setUrl, setOptions };
}

Calling Fetch with Async Await

I like async/await syntax over Promise syntax, so let’s use the former! This, of course, works just as well using then, catch, and finally rather than async/await.

import { useState } from 'React';

function useFetch(initialUrl, initialOptions) {
  const [url, setUrl] = useState(initialUrl);
  const [options, setOptions] = useState(initialOptions);
  const [data, setData] = useState();
  const [error, setError] = useState();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    setError(undefined);

    async function fetchData() {
      try {
        const res = await fetch(url, options);
        const json = await res.json();
        setData(json);
      } catch (e) {
        setError(e);
      }
      setLoading(false);
    }
    fetchData();
  }, [url, options]);

  return { data, error, loading, setUrl, setOptions };
}

That was a lot! Let’s break it down a bit. When we run our effect, we know that we’re starting to fetch data. Therefore we set our loading variable to true and we clear our any errors that may have previously existed.

In our async function, we wrap our fetch request code with a try/catch block. Any errors we get we want to report to the user, so in our catch block we setError to whatever error is reported.

In our try block, we do a fairly standard fetch request. We assume our data being returned is json because I’m lazy, but if we were trying to make this the most versatile hook we would probably give our users a way to configure the expected response type. Finally, assuming all is successful, we set our data variable to our returned JSON!

Using The Hook

Believe it or not, that’s all there is to creating our custom hook! Now we just need to bring it into a sample app and hope that it works.

In the following example, I have an app that loads any github user’s basic github profile data. This app flexes almost all the features we designed for our hook, with the exception of setting fetch options. We can see that, while the fetch request is being loaded, we can display a “Loading” indicator. When the fetch is finished, we either display a resulting error or a stringified version of the result.

We offer our users a way to enter a different github username to perform a new fetch. Once they submit, we use the setUrl function exported from our useFetch hook, which causes the effect to run and a new request to be made. We soon have our new data!

const makeUserUrl = (user) => `https://api.github.com/users/${user}`;

function App() {
  const { data, error, loading, setUrl } = useFetch(makeUserUrl('nas5w'));
  const [user, setUser] = useState('');

  return (
    <>
      <label htmlFor="user">Find user:</label>
      <br />
      <form
        onSubmit={(e) => {
          e.preventDefault();
          setUrl(makeUserUrl(user));
          setUser('');
        }}
      >
        <input
          id="user"
          value={user}
          onChange={(e) => {
            setUser(e.target.value);
          }}
        />
        <button>Find</button>
      </form>
      <p>{loading ? 'Loading...' : error?.message || JSON.stringify(data)}</p>
    </>
  );
}

Feel free to check out the useFetch hook and sample application on codesandbox here.

Concluding Thoughts

Writing a custom React hook can be a fun endeavor. It’s sometimes a bit tricky at first, but once you get the hang of it it’s quite fun, and can result in really shortening and reducing redundancy in your component code.

If you have any questions about this hook, React, or JS in general, don’t hesitate to reach out to me on Twitter!

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli