Peter Ekene Eze Learn, Apply, Share

How to handle async side effects in 2019

4 min read 1371

A guide to handling async side effects in React in 2019

Handling async operations has been a major concern for developers in the React ecosystem.

There’s been a bunch of different approaches to handling async operations, including Redux-Saga, but in this article we’ll be focusing on what I think is currently the best approach: using react-async.

We’ll also make comparisons between the react-async library and other existing methods of handling asynchronous side effects in React.

What is React Async?

React Async is a promise-based tool that lets you handle promises and fetch data declaratively.

It makes it easy to handle asynchronous UI states without assumptions about the shape of your data or the type of request.

React Async consists of a React component and several Hooks. You can use it with fetch, Axios, GraphQL, and other data fetching libraries.

React Async relies on using a declarative syntax, JSX, and native promises to resolve data closer to where you need them in your code (for example, on the component level), unlike other systems like Redux where resolving data happens at a higher level in your application using things like actions and reducers.

React Async Usage

To use React Async like in the example below, we will import useAsync from react-async

Then we can create our asynchronous function, which receives a signal as a parameter. The signal is our AbortController API, which provides a way for us to cancel the fetch call we make if there’s ever a need to do so.

In our component, we call useAsync and pass on our asynchronous function.

Calling useAsync returns an object that we can de-structure into three important values: data, error, and isPending.

These values tell us about the state of our asynchronous function — whether it’s still pending, errored out, or successful.

We can use each of these values to render an appropriate UI for the user:

import { useAsync } from "react-async"
// You can use async/await or any function that returns a Promise
const asyncFn = async ({ signal }) => {
  const res = await fetch(`/api/users`, { signal })
  if (!res.ok) throw new Error(res.statusText)
  return res.json()
}
const MyComponent = () => {
  const { data, error, isPending } = useAsync({ promiseFn: asyncFn })
  if (isPending) return "Loading..."
  if (error) return `Something went wrong: ${error.message}`
  if (data)
    <ul>
      {data.users.map(user => <li>{user.name}</li>)}
    </ul>
)
return null

There are a few documented ways of using React-Async:

  • As a Hook
  • With useFetch
  • As a component
  • As a factory
  • With helper components
  • As static properties of

I’ll be briefly touching the first three methods just to give you an idea of these implementations, but feel free to reference the official usage guide to get an in-depth view of each of the methods.

React Async as a Hook

React-Async provides a Hook called useAsync. Within your component, you can call this Hook like so:

import { useAsync } from "react-async";

const MyComponent = () => {
  const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })
  //...
};

React Async with useFetch

With useFetch, you are creating an asynchronous fetch function which can be run at a later time in your component:

import { useFetch } from "react-async"
const MyComponent = () => {
  const headers = { Accept: "application/json" }
  const { data, error, isPending, run } = useFetch("/api/example", { headers }, options)
  // You can call "handleClick" later
  function handleClick() {
    run()
  }
<button onClick={handleClick}>Load</button>
}

React Async as a component

Here’s where React Async really shines with JSX:

import Async from "react-async"
const MyComponent = () => (
  <Async promiseFn={load}>
    {
      ({ data, error, isPending }) => {
        if (isPending) return "Loading..."
        if (error) return `Something went wrong: ${error.message}`
        if (data)
          return (<div> { JSON.stringify(data, null, 2) }</div>)
        return null
      }
    }
  </Async>
)

You have to pass a function to the Async component as a child.

As you can see, this function will evaluate different node values based on the state of the asynchronous function we provided as props to Async.

React Async vs useEffect

useEffect in combination with Async/Await isn’t quite as convenient as React Async, especially when you start thinking of race conditions, handling clean-ups, and cancelling pending async operations.

React Async handles all these thin gs for you in a very efficient manner.

Let’s take a look at a typical example of handling race conditions with useEffect and Async/Await:

const [usersList, updateUsersList] = useState();
useEffect(() => {
  const runEffect = async () => {
    const data = await fetchUsersList(filter);
    updateUsersList(data);
  };
  runEffect();
}, [updateUsersList, filter]);

In the case above, if for any reason we have to call the useEffect twice and the second call to fetchUsersList resolves before the first, we’d have an outdated “updated” list.

You can fix this by adding a way to prevent the updateUsersList call from happening when you deem it necessary, however, such methods might not scale well with multiple await expressions.

On the other hand, you don’t have to wory about cancelling unresolved requests or handling proper race conditions when you use React Async because React already handles that for you:

import { useAsync } from "react-async"
// You can use async/await or any function that returns a Promise
const fetchUsersList = async ({ signal }) => {
  const res = await fetch(`/api/users`, { signal })
  if (!res.ok) throw new Error(res.statusText)
  return res.json()
}
const filteredUsers = (users) => {
  // Filter users ...
}
const MyComponent = () => {
  const { data, error, isPending } = useAsync({ promiseFn: fetchUsersList})
  if (isPending) return "Loading..."
  if (error) return `Something went wrong: ${error.message}`
  if (data)
  <ul>
    { filteredUsers(data.users).map(user => <li>{user.name}</li>) }
  </ul>
)
return null

In the above code snippet, any time we call fetchUsersList, we would be re-rendering the MyComponent component, which means we’ll always have the state we expect.

Also, React Async does a clean up internally for us and cancels unresolved promises using the AbortController API (i.e the signal variable being passed to fetchUsersList function,) so we don’t have to worry about race conditions and cancelling unresolved promises that we no longer need.

If your application is really basic and adding a 14kb library to handle async operations doesn’t make sense, then you could settle for a slightly more advanced implementation of useEffect.

In my opinion, React Async is already pretty light-weight and has lots of great benefits in addition to being well-tested.

So unless the gains you get from saving 14kb is crucial, you might want to use React Async.

React Async vs Redux-Saga

Redux-Saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures: redux-saga.js.org.

Redux-Saga requires a lot more steps to get started than React Async.

That’s because it’s a Redux middleware, which means you have to set up Redux for it.

The idea of Redux is to have a centralized state for all or major parts of your application. That way, you can update your state through dispatching actions. For instance:

const Counter = ({ value }) =>
  <div>
    <button onClick={() => store.dispatch({type: 'INCREMENT_ASYNC'})}>
      Increment after 1 second
    </button>
    <hr />
    <div>
      Clicked: {value} times
    </div>
  </div>

Redux-Saga helps you make network calls or perform other asynchronous side effects by relying on “ES6 Generators”:

function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

As you can see, where you do your side effects is far removed from your components. You trigger updates by dispatching actions within your component. Then, the updated state comes in through your props.

Pretty standard stuff, but very different and less intuitive than what React Async gives you.

Conclusion

  • With React Async, you don’t need to assume how your data is going to look as you have to do with Redux. It’s just like how you’d typically use Promises.
  • With React Async, you resolve your data closer to where you need it, making it clearer to understand what’s going on.

You don’t have to understand a fairly complex structure with constructs like reducers and actions — you utilize what you already know and use within your components – JSX, Promises, and Hooks.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Peter Ekene Eze Learn, Apply, Share

One Reply to “How to handle async side effects in 2019”

  1. Peter, very interesting article! I will need to check out the technologies you mentioned that I’m not already familiar with.

    Given your great knowledge on this subject matter, may I ask for your advice? I’ve successfully built several React modules with functional components which make all async calls from within a useEffect. I’ve done it this way because this is what the official documentation & all articles I’ve read say should be done. A new colleague feels that there’s nothing wrong with making all async GET calls (ex. axios.get) from within, say, a button event handler. I don’t have a solid technical explanation to give him as to why this approach could lead to problems. Might you?

Leave a Reply