DEV Community

Cover image for Writing Your Own useFetch Hook in React
Nick Scialli (he/him)
Nick Scialli (he/him)

Posted on • Originally published at typeofnan.dev

Writing Your Own useFetch Hook in React

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!


If you enjoy this post, please give it a πŸ’“, πŸ¦„, or πŸ”– and consider signing up for πŸ“¬ my free weekly dev newsletter


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
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
jantimon profile image
Jan Nicklas • Edited

Your useEffect is using an async function which can be problematic.
If your component unmounts or your effect dependencies like [url, options] change while your ajax is running the "old" effect run will still manipulate the state.

The worst possible case is that the first call is slower than the second ajax call and your state will end up with wrong data.

Here is a small trick to prevent those bugs by checking after your awaits if the effect is still running:

useEffect(() => {
    let isEffectRunning = true;

    async function fetchData() {
      try {
        const res = await fetch(url, options);
        const json = await res.json();
        // Stop here if the effect is outdated:
        if (!isEffectRunning) { return }
        /* ... */
      } catch(e) {
        /* ... */
      }
    }
    /* ... */
    return () => {
       isEffectRunning = false
    }
}, [url, options]);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jeremydmarx813 profile image
Jeremy Marx

would it be a good idea to have a setTimeout for fetching the data from the Api, so the client is trying to fetch data from the API before the user has finished typing their name? If so, would you put that in the app component or the custom hook?