Web applications have to deal with a lot of asynchronous operations. Loading and updating data is something you have to do over and over again. There’s two common approaches to this: using a global state manager (e.g. Redux) or using setState locally.
Although I’ve been a fan of Redux in the past mostly for its functional style, it tends to be too much for your average application. It’s a steep learning curve and quite a bit of boilerplate, but most of all it distances the data fetching from the data presentation. I like to keep things simple, so I went back to the basics: local setState.
The common place to perform data fetching in a React component is in the componentDidMount lifecycle method. Just run fetch and use setState to store the result. This is simple enough (and very effective) but leaves a lot to be desired. What about showing some sort of loading indicator? What about error handling? In practice you’ll want to enhance your fetch with a bunch of metadata. You should also consider promise cancellation on unmount, as well as deal with race conditions between consecutive fetches (FiFo).
React Async is a React component built to deal with local asynchronous state. It handles (native) promise resolution, enhances it with metadata (
finishedAt) and deals with the intricacies you wouldn’t normally think about. Error handling and retrying a promise is a breeze. You can find it as react-async on npm.
React Async leverages the Render Props pattern for ultimate flexibility as well as the new Context API for ease of use. You can even build your own instance of Async that’s preconfigured to deal with a certain promise and uses a name that’s fitting for your domain language. Here’s the basic example:
While using render props is very powerful, writing if statements and return keywords in JSX like that doesn’t feel right. That’s why React Async comes with several helper components:
<Async.Rejected>. Each of these components will only render its children when appropriate. This simplifies the above example:
The helper components accept regular React elements or a function as children. When you provide a function, you’ll again receive render props including the data, error and metadata.
React Async uses the React Context API to pass data down to the helper components. This means there is no need for them to be direct children and you can have multiple instances of the same helper. If you want, you can even combine the render props version of
<Async> with helper components.
One downside of the way we use Context is that it’s a single Context for all instances of
<Async>. This means when you nest
<Async> inside another
<Async>, you could run in to the problem of them overwriting each others context data. That’s why we also export a
createInstance function which will create a unique context. It also accepts default props so you can preconfigure an instance. Here’s how that works:
Of course you can name this instance anything you want, so it makes sense to name it according to the data it deals with.
Mutations and optimistic updates
Besides fetching data, you oftentimes want to update data as well. Usually this means doing a POST request. Obviously you wouldn’t want this to be triggered in
<Async> also accepts a
deferFn. It’s identical to
promiseFn, with the exception that
deferFn must be manually invoked:
The reason it doesn’t just reuse
promiseFn with a flag to disable direct invocation is that it allows you to use both
deferFn at the same time. Fetch the original (form) data using
promiseFn, then update that data through
deferFn. Keep in mind that
deferFn should return the same type/shape of data that
deferFn in place, it becomes trivial to implement optimistic updates. Basically we’re going to act as if the update succeeded, immediately updating the data to what we think it will become, while waiting for the actual promise to resolve. To do this
<Async> exposes the
setData method as a render prop:
setData effectively updates the value of
data immediately, while
run triggers the
deferFn which will eventually update
data to the actual value (which should be identical).
There’s more work to be done to make React Async capable to handle every possible situation. Specifically nested, parallel dependent promises are still less than ideal. You can easily have one Async wait for another by wrapping it in
<Async.Resolved>, but that will run the promises in sequence instead of in parallel. Perhaps this is an edge case, but the end game is that React Async is your one-stop-shop for dealing with Promises in React, so it should support even these use cases.
Finally, I would like to thank Andrey Popp, the original owner of
react-async on npm, for handing over ownership of the package so I could repurpose the name for my package. I hope the React community will enjoy the effort.