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
}
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!
Top comments (2)
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:
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?