Managing Asynchronous Code in React
Meet Suspense & Error Boundaries
Introduction
Traditionally, dealing with asynchronous code in React has required some boilerplate:
- Make a wrapper component to show a pending or rejected state.
- Make our components keeping track of pending and rejected state and inform the wrapper component about state changes.
But ideally, we want:
- To care only about the fulfilled state in our components.
- To abstract away the pending and rejected state.
Here, I am going to show you how to achieve just that and how to structure asynchronous code in a modern way.
Tools
We are going to use the Suspense component and Error Boundaries. They have been available since React 16.6.
Suspense
The Suspense component will display a fallback
user interface until all of its children will have their state fulfilled:
<Suspense fallback="Waiting...">
<ChildComponent1 />
<ChildComponent2 />
</Suspense>
Error Boundary
From now on, I am going to use the Exception component as an Error Boundary implementation. It will display a fallback
user interface if any of its child components will throw
an exception:
<Exception fallback="An error has occurred!">
<ChildComponent1 />
<ChildComponent2 />
</Exception>
Example
Suppose we want to fetch users’ data from a remote resource:
const fetchUsers = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
const users = await response.json();
console.log("Users data", users);
return users;
};
I will use makeSuspendableHook to integrate our asynchronous code within Suspense and Error boundary:
const useUsers = makeSuspendableHook(fetchUsers());
In our component, all we should care about is the actual data and its representation:
const Users = () => {
const users = useUsers();
return (
<div>
List fetched users:
<ul>
{users.map(({ name }) => (
<li>{name}</li>
))}
</ul>
</div>
);
}
Finally, we will wrap everything together:
export default () => (
<Exception fallback="An error has occurred">
<Suspense fallback="Waiting...">
<Users />
</Suspense>
</Exception>
);
Play with web example at codesandbox.io
Play with native example at snack.expo.io
That is it!
Details
The Suspense and Error Boundaries are like try-catch
blocks for components.
- The Suspense will
catch
any promise. It will wait until the promise will be fulfilled rendering its children or in case of the promise being rejected it willthrow
the rejection value. - The Error Boundary will
catch
anything.
Here is what hypothetical code might look like
try {
<Component1 />
<Component2 />
}
catch (promise) {
<i>Waiting...</i> // until promise will be settled
}
catch (anything) {
<b>An exception: "{anything}" has occurred!</b>
}
Therefore you should enclose the Suspense within Error Boundary and not vice versa, as the Error Boundary will catch a promise before the Suspense.
Of course, you can make many layers of nesting of Suspense and Error Boundaries, fine-tuning the desired outcome.
This throw
and catch
mechanics will only work during React’s rendering phase. So if you will try to throw
in some event, it will not work. You can useEventThrow in this case.
Thank you!