Handling network request state in React with a custom Hook
React Hooks can be used to manage the life cycle of a network request in our components. When I say life cycle I'm referring to the common pattern of presenting a loading indicator to a user and then updating the user interface upon success or failure.
This can be used as an alternative to tracking such state in a store, like Redux
Keeping track of the request state
Starting with the actual state of the request, we can opt for a string that matches the naming scheme of Promise object statuses:
const requestStatus = 'IDLE' | 'PENDING' | 'FULFILLED' | 'REJECTED';
And actually, I find this a bit nicer to manage as an object rather than four separate constants (mostly so I can leverage them in TypeScript):
const requestStatus = {
IDLE: 'IDLE',
PENDING: 'PENDING',
REJECTED: 'REJECTED',
FULFILLED: 'FULFILLED',
};
With that decided we can now set the value based on the state of a fetch request from our component.
In this example below I'm using the excellent wretch
library
import {useEffect, useState} from 'react';
import wretch from 'wretch';
const MyComponent = () => {
// Initial state is 'IDLE', as we may not always trigger the request
// immediately on component render. It could happen in an event for example
const [requestStatus, setRequestStatus] = useState(requestStatus.IDLE);
// Let's make this request on initial render though...
useEffect(() => {
setRequestStatus(requestStatus.PENDING);
wretch('some/endpoint')
.then(() => setRequestStatus(requestStatus.FULFILLED))
.catch(() => setRequestStatus(requestStatus.REJECTED));
}, []);
// And handle our states visually
if (requestStatus === requestStatus.PENDING) {
return <Loading />;
}
if (requestStatus === requestStatus.REJECTED) {
return <Error />;
}
return <p>The content of my component</p>;
};
Extracting the logic into a custom Hook
Our above example works quite nicely and it's also a great candidate for a custom Hook that other components can make use of. Let's create that now:
import {useState} from 'react';
export const requestStatus = {
IDLE: 'IDLE',
PENDING: 'PENDING',
REJECTED: 'REJECTED',
FULFILLED: 'FULFILLED',
};
export const useRequestStatus = () => {
const [status, setRequestStatus] = useState(requestStatus.IDLE);
return {
setStatusIdle: () => setRequestStatus(requestStatus.IDLE),
setStatusPending: () => setRequestStatus(requestStatus.PENDING),
setStatusFulfilled: () => setRequestStatus(requestStatus.FULFILLED),
setStatusRejected: () => setRequestStatus(requestStatus.REJECTED),
isStatusIdle: status === requestStatus.IDLE,
isStatusPending: status === requestStatus.PENDING,
isStatusFulfilled: status === requestStatus.FULFILLED,
isStatusRejected: status === requestStatus.REJECTED,
};
};
Why move this into a Hook?
Most obviously the logic can now be shared with other components that also want to handle network requests but a bigger win is that components no longer need to know about how the state is managed.
Instead we expose simple setter functions that don't require any arguments and booleans for each possible state of the request, only one of which can be true at any point during a component render.
I recommend applying this rule of abstraction to any custom Hooks where possible, and really leverage the freedom to return whatever you wish back to the component.
Using the custom useRequestStatus
Hook
Let's add it to our MyComponent
example:
import {useEffect, useState} from 'react';
import wretch from 'wretch';
import {useRequestStatus} from './useRequestStatus';
const MyComponent = () => { const { setStatusPending, setStatusRejected, setStatusFulfilled, isStatusPending, isStatusRejected, } = useRequestStatus();
useEffect(() => {
setStatusPending();
wretch('some/endpoint')
.then(() => setStatusFulfilled()) .catch(() => setStatusRejected()); }, []);
if (isStatusPending) { return <Loading />;
}
if (isStatusRejected) { return <Error />;
}
return <p>The content of my component</p>;
};
The well named values from useRequestStatus
make this logic even easier to
read and understand. A nice refactor.