Asynchronous Functional Programming Using React Hooks

Stefan Nagey
Capbase Engineering
5 min readJan 12, 2019
How to use async/await with stateless React components

In the first three parts of this series, we’ve looked at why functional programming is important to react, how to manage state in a stateless component, and how to replace the flux pattern or replace redux with React hooks. In this part, we’re going to look at how to handle asynchronous tasks in the functional programming world, and how you can use the async/await pattern when working with React Hooks and stateless components.

I simply have to wait for something — being async in a synchronous functional world

As you’ve noticed, everything that we’ve talked about until now has been completely linear and synchronous. If we’re just working with a simple webpage, that’s all well and good, but what if we need to use a backend, what if we need to venture into the asynchronous world?

Luckily, useEffect is made for exactly that purpose.

The React.useEffect hook takes a function as an argument and it will call that function after the main render cycle has completed, meaning that you can use it to complete async operations, like calls to an API remote, whether it be GraphQL or RESTful (or SOAP or really whatever you like).

It’s generally best practice to define a function that you call from your effect and then do your async operations there. It allows you to use the async/await paradigm, which, I think we can all agree, makes life easier.

Let’s take a look at a simple async handler, we’ll call it updateProductBackend.

const updateProductBackend = async (product) => {
return await updateBackend(product);
}

This is very simple, it doesn’t update our UI or anything like that, however, since closures are a thing that exist, we can use any variable in the current scope (including, say, a update function pointer from a useState call or the dispatch method from a useContext call where the context provider was initialized with useReducer to do that for us. Or, if we don’t want to use closures, then we can pass those items into this handler and use them there, they’ll still work.

That might make this function look something like this:

const updateProductBackend = async (product, dispatch) => {
const result = await updateBackend(product);
if (result.success) {
dispatch({ type: “success” });
} else {
dispatch({ type: “failure” });
}
}

Of course, for this to be meaningful, our middleware reducer has to be extended to handle the success and failure action types.

The React.useEffect call

Let’s now take the async handler we defined in the previous section and put it to use inside a useEffect call.

In the body of any component, we can now add the following code:

useEffect(() => updateProductBackend(product, dispatch));

Let’s look at our ProductDisplay component from earlier, and see how this fits in.

const ProductDisplay = () => {
const {
state: product,
dispatch
} = React.useContext(ProductsContext);

useEffect(() => updateProductBackend(product, dispatch));

return <Product data={product} />;
};

This is fine, but the eagle eyed reader will notice that this effect will call our backend every single time that our state changes. We probably don’t want that.

Let’s say that we’ve got a woot like item that increments our price by a penny each time someone buys.

To do this, we’ll need a buy button, and then we’ll need to handle that price increment, but only if the product is bought.

Let’s accomplish this with a state that tells us if the user intends to purchase (we’ll call it purchaseIntent), and then an effect to do the purchase and to increment the price.

const ProductDisplay = () => {
const {
state: product,
dispatch
} = React.useContext(ProductsContext);

const [purchaseIntent, setPurchaseIntent] = useState(false);

useEffect(() => {
const newProduct = {
…product,
price: product.price + 0.01
};
if (purchaseIntent) {
updateProductBackend(newProduct, dispatch);
purchaseProduct(product, dispatch);
}
return null;
},
[ purchaseIntent ]
);

return (
<React.Fragmemt>
<Product data={product} />
<button onClick={() => setPurchaseIntent(true);} />
);
};

What you should note here is the fact that I’ve passed purchaseIntent as the second argument to useEffect. What this does is to tell useEffect that it needs to execute the first argument if the value it receives in the second argument is different from the last value received. Given that before the first time, it’s undefined, it will run the first time as well (hence the check on purchaseIntent).

It’s also important here to note that if your call to useEffect has any long-running consequences, you need to return a callback that will terminate these processes.

Wrapping it all up

As you can see through these examples, hooks allow us to do absolutely everything that we would in the classic react lifecycle method, but everything is in a single flow, where we don’t have to worry about all the complexities that the object model exposed us to.

This allows each piece to become a more discrete unit (which, as such, can be unit tested), and to keep all of the pieces simple as well.

It also has the wonderful side effect of being faster. Since with this approach, your functions are not long-running and don’t contribute to the heap size like objects do, your app will have a lower overall memory footprint and run more smoothly everywhere it runs.

Read More — Other parts in this series

If you haven’t seen it, please also read part 1, discussing the reasons for functional programming and stateless components in react, part 2 managing local state with React Hooks, and part 3 replacing your redux action/reducer pattern with the react Hooks useContext and useReducer hooks.

Coming up next will be the final part in this series, exploring the various gotchas of working with React Hooks, React Concurrent Mode, and Functional Programming in Javascript.

About the Author

Stefan Nagey is Co-Founder of Capbase and Dharma. In a previous life he has variously promoted motion pictures, been a diplomat and registered foreign agent, and run payment processing in the online gambling space. He variously writes about engineering and the headaches of being a startup founder in the 21st century.

--

--

Stefan Nagey
Capbase Engineering

Data geek. Capbase.com and Dharma.ai Co-Founder. Passionate about scalability, governance, cycling, and poker.