A Custom Hook For Loading State

06/08/20191 Min Read — In React, Hooks

I've written a lot of React components over the past couple years that fetch or update data with a call to a server. Not knowing how long these server calls are going to take, I like to indicate to the user that we are working on it. I often do this by putting the component in a loading state.

One of these components might look something like this:

class LoadingStateExample extends React.Component {
state = {
loading: false
};
setLoading = isLoading => {
this.setState({ loading: isLoading });
};
handleSubmit = () => {
this.setLoading(true);
makeSuccessfulApiCall().finally(() => {
this.setLoading(false);
});
};
render() {
const { loading } = this.state;
return (
<div>
{loading && <p>{"Loading..."}</p>}
<button disable={loading} onClick={this.handleSubmit}>
Submit
</button>
</div>
);
}
}

A bit of a micro-pattern has emerged across all of these different components. Notice the submit handler:

handleSubmit = () => {
this.setLoading(true);
makeSuccessfulApiCall().finally(() => {
this.setLoading(false);
});
};

Here is the micro-pattern in pseudo-code:

handleSubmit = () => {
// turn on the component's loading state
// do a series of (potentially complex) async API interactions
// when everything is done, turn off the loading state
};

It's a simple pattern that is highly effective for most API-connected components. This is a straightforward way to give a reactive and responsive element to our app.

With the advent of the Hooks API, we can tighten up this component a bit:

const HooksLoadingExample = () => {
const [loading, setLoading] = useState(false);
const handleSubmit = () => {
setLoading(true);
makeSuccessfulApiCall().finally(() => {
setLoading(false);
});
};
return (
<div>
{loading && <p>{"Loading..."}</p>}
<button disable={loading} onClick={handleSubmit}>
Submit
</button>
</div>
);
};

Cool, we've used hooks. We are on the bleeding edge of modern React development. We've also shaved about 10 lines of code off the class component example. This didn't have any effect on the loading state pattern -- those lines of code still look the same.

Not just that, we still have this responsibility to manually manage the loading state. We can shift this responsibility, or at least the individual pieces of it, into a custom hook.

const useLoadingWrapper = initialLoadingState => {
const [loading, setLoading] = useState(initialLoadingState);
const loadingWrapper = callback => {
setLoading(true);
return callback().finally(() => {
setLoading(false);
});
};
return [loading, loadingWrapper];
};

This is bound to look familiar. It is our loading state pattern, codified as a custom React hook. Also, it's just a function. A function we can reuse wherever it's needed so that we aren't reinventing the loading state wheel over and over across our app.

Here is how we can use the useLoadingWrapper hook:

const LoadingWrapperExample = () => {
const [loading, loadingWrapper] = useLoadingWrapper(false);
const handleSubmit = () => {
loadingWrapper(makeSuccessfulApiCall);
};
return (
<div>
{loading && <p>{"Loading..."}</p>}
<button disable={loading} onClick={handleSubmit}>
Submit
</button>
</div>
);
};

The above handleSubmit function is a bit over-simplified. A more realisitic example might look something like this:

const handleSubmit = () => {
loadingWrapper(() => {
return makeApiCall().then(resp => {
const { data } = resp;
const someData = data.filter(filterFunc);
setData(someData);
}).catch(err => {
setError(err.message);
});
});
};

The loadingWrapper allows us to think of our data fetching logic apart from managing the loading state. Promise chains are tricky enough. If we can remove a slice of logic, that is a win.

If you're interested, you can check out a live code example on Codesandbox.

I'm just starting to think about the power of custom React hooks. Let me know what you think -- I'm on twitter.

Did you enjoy this post? Get emails from me with my latest posts by signing up for my newsletter.