Modern day user interfaces are expected to handle asynchronous state changes. Whether it is server communication, offloaded computations, or permission-based browser APIs, developers need to convey to their customers what is happening, has happened, is going to happen, and what went wrong.

A common pattern arises in React applications, where — in addition to the asynchronous functionality — the component’s local state must update for each step in the state machine: uninitiated, pending, fulfilled, and rejected.

Waiting can be hard. Let’s do it together.

When a common pattern begins to find itself repeated throughout your codebase, it’s a safe time to DRY your code up, abstract away that workload, and prevent error by maintaining that logic in a single, reusable location. In this instance, a React hook can simply (and with strong TypeScript support) automate the update of a React component in response to the call of an asynchronous function.

useAsyncFunction ( npm install use-async-function ) takes an asynchronous function and couples its state to your component’s local state.

Let’s look at an example:

import React from 'react';

import useAsyncFunction, { State } from 'use-async-function'; function Users() {

const loadUsers = useAsyncFunction(

() => fetch('/users'),

); switch (loadUsers.state) {

case State.Fulfilled:

return <UserList>{loadUsers.response}</UserList>; case State.Rejected:

return <ErrorMessage>{loadUsers.error}</ErrorMessage>; case State.Pending:

return <LoadingSpinner />; // If this function has not been called, let's call it.

default: {

loadUsers();

return <LoadingSpinner />;

}

}

}

In a stateless example, () => fetch('/users') will fetch users, but never rerender the component — not when the request dispatches, the response returns, or an error occurs. Oftentimes, you’ll have to manually create your own error and loading states or an asynchronous state machine, then manually update the state with each call.

In the above code, the state machine ( loadUsers.state ) is handled automatically, as is the necessary update process of the component.

What are the states of an async function? 🤷‍♀️

Defining an asynchronous function as a function that returns a Promise, the states of an asynchronous function are similarly the states of a Promise.

Pending: The pending state of an asynchronous function denotes that the final value is not yet available. This is the only state that may transition to one of the other two states.

The pending state of an asynchronous function denotes that the final value is not yet available. This is the only state that may transition to one of the other two states. Fulfilled: The fulfilled state of an asynchronous function denotes that the final value became available. An asynchronous function is not guaranteed to ever become fulfilled, such as in the event of an error. This may be any value, including undefined .

The fulfilled state of an asynchronous function denotes that the final value became available. An asynchronous function is not guaranteed to ever become fulfilled, such as in the event of an error. This may be any value, including . Rejected: The rejected state of an asynchronous function denotes that an error prevented the final value from being determined. This may be any value, including undefined , though it is generally an Error object, like in exception handling.

The rejected state of an asynchronous function denotes that an error prevented the final value from being determined. This may be any value, including , though it is generally an object, like in exception handling. undefined : Demonstrating parity with MDN’s definition of Promise states, a Promise that has yet to be instantiated has no state at all, and the state machine value is simply undefined .

How do I use it? 💪

To couple your asynchronous function’s state to your React component’s local state, simply wrap it in useAsyncFunction , which you can find on NPM and open source on GitHub:

npm install use-async-function

The useAsyncFunction hook returns an exact copy of the original asynchronous function, so you still invoke it the same way.

const statelessBake = (item, duration) => {

return new Promise((resolve, reject) => {

if (isBaked(item)) {

reject(`${item} is already baked!`);

return;

}

setTimeout(() => {

const bakedItem = bakedGoods.get(item);

resolve(bakedItem);

}, duration);

});

}; const statefulBake = useAsyncFunction(statelessBake);

In the above, a call to statelessBake('pie', 60) returns a Promise<BakedGood> , but it does not track when the pie has finished baking or if an error has occurred. In order to convey this information to your users, it requires a then and catch , each of which must set the React component’s local state with the corresponding state change — is it loading, has it finished loading, what did it load, or what error took place?

By comparison, a call to statefulBake('pie', 60) is all you need to do. statefulBake.state will tell you if it is still baking, if an error occurred during the baking process, and what the final product of your baking function was. Your component will rerender as the state machine changes, through no additional code of your own.

Conclusion 🔚

If you have any questions or great commentary, please leave them in the comments below.

To read more of my columns, you may follow me on LinkedIn and Twitter, or check out my portfolio on CharlesStover.com.