Suspense in React 18
Arpit
Posted on August 21, 2022
What is Suspense?
The <Suspense />
component is a feature that was introduced along with React.lazy in React 16.6, to enable client side code splitting to load react components only when they're needed.
With React 18, Suspense is a lot more general and works for any asynchronous action you may wish to perform in your components, for e.g. data fetching.
For React 18, according to the release notes, you can technically use suspense for data fetching but for several libraries that support it, it's still an experimental feature.
Suspense is able to detect when your component is "suspended" and renders a fallback for it. The question you may have is, what does it mean for a component to be "suspended"?
How to suspend a component
A suspended component is one that threw a promise that is yet to be fulfilled. Now that sentence may be confusing to you. What do you mean "threw" a promise?
Below is a simple example of a suspended component, this is unusual React code, and you will never need to write this directly, but this is exactly how React knows when a component is suspended.
function Component() {
throw new Promise((resolve) => {
console.log('this is a promise that will never resolve');
});
return <div>Hello World</div>;
}
In most code bases, we are just used to throwing an error like this:
throw new Error('something went wrong');
But throw
in JavaScript is very generic, it will throw whatever you want, it doesn't have to be an error. Along with throw
, we use try/catch
in to "catch" what was thrown, so we can gracefully handle the error.
<Suspense />
from a developer's perspective, works just like a catch
block and tells react that this component is suspended, and we can't yet render it, it has to be handled in some other way.
That other way is to render a fallback component. The most common use case will be to show a loading indicator.
So instead of using isLoading
state as we're used to, we will use <Suspense />
, and its fallback
prop.
Example using isLoading
.
function App() {
const { data, isLoading } = useDataFetcher();
if (isLoading) {
return <div>loading...</div>;
}
return <Component />;
}
Example using Suspense
.
function App() {
return (
<React.Suspense fallback={<div>loading...</div>}>
<Component />
</React.Suspense>
);
}
How to write a suspender function
Here is a naive implementation of a function that suspends a component and then resumes after our async action
function is "fulfilled".
interface Response<T> {
status: 'success' | 'pending' | 'error';
data: T | null;
}
/**
* A promise tracker that will be updated
* when promise resolves or rejects
*/
const response: Response<unknown> = {
status: 'pending',
data: null
};
/**
* This is our suspender function
* that throws promise if it is not fulfilled yet
*/
export function suspend<T>(fn: () => Promise<T>) {
/**
* suspender is the promise we will throw
* so react can re-render when it is fulfilled
*/
const suspender = fn().then(
(res) => {
response.status = 'success';
response.data = res;
},
(error) => {
response.status = 'error';
response.data = error;
}
);
switch (response.status) {
case 'pending':
throw suspender;
case 'error':
throw response.data as T;
default:
return response.data as T;
}
}
In this code example we keep track of a global response
variable that tracks the state of our promise, suspender
is a new promise that is thrown if the status of our argument is still 'pending'
. Otherwise, we can return the resolved data.
When the component is suspended, React has access to the suspender
promise. When it is "resolved" or "rejected", React will attempt to re-render the component and this time since data will available we do not need to rely on our fallback.
In our React component, we will use the suspend
function like this.
import * as React from 'react';
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const action = async () => {
await sleep(2000);
return { greeting: 'hello world' };
};
function Component() {
const data = suspend(action);
return <div>The Greeting is - {data.greeting}</div>;
}
function App() {
return (
<React.Suspense fallback={<div>loading...</div>}>
<Component />
</React.Suspense>
);
}
In the above example <Component />
will throw a promise and in <App />
, <React.Suspense />
will catch it, and render the given fallback instead.
This suspend
function has several issues though. Component
currently does not accept any props and action
does not accept any arguments that may be derived from those props, we cannot handle cases where promises are recreated for new function calls.
As a user just building UI components, You will probably never have to worry about these problems since they're supposed to be handled by external libraries.
Problems like caching or multiple promises are handled by whichever data fetching solution you use or something generic like suspend-react which will handle them for you.
Let's rewrite our example with suspend-react
.
import * as React from 'react';
import { suspend } from 'suspend-react';
const action = async (name) => {
await sleep(2000);
return { greeting: `hello ${name}` };
};
function Component(props) {
const data = suspend(() => action(props.name), [props.name]);
return <div>The Greeting is - {data.greeting}</div>;
}
suspend-react
works as a suspender function and has a global cache for your actions.
It accepts a list of keys to cache the status of a promise or the result of its execution.
When it comes to data fetching, there are way more specific problems that some libraries like swr will take care of.
Let's see an example with swr
.
import * as React from 'react';
import useSWR from 'swr';
function Component() {
const { data } = useSWR('/api/user', fetcher, { suspense: true });
return <div>hello, {data.name}</div>;
}
function App() {
return (
<React.Suspense fallback={<div>loading...</div>}>
<Component />
</React.Suspense>
);
}
As you can see, the complexity of writing a suspender function is abstracted away from your code, and you end up with a nice clean API surface.
Nested Suspense Boundaries
Just like try/catch
blocks, <Suspense />
boundaries can be nested.
You can have a single suspense boundary for several components.
import * as React from 'react';
function App() {
return (
<React.Suspense fallback={<Loader />}>
<UserAvatar />
<UserName />
</React.Suspense>
);
}
Or Several suspense boundaries for each component to handle their suspended state differently.
import * as React from 'react';
function App() {
return (
<div>
<React.Suspense fallback={<Loader />}>
<UserAvatar />
</React.Suspense>
<React.Suspense fallback={<Loader />}>
<UserName />
</React.Suspense>
</div>
);
}
Which strategy you choose will depend on your designs, UX and other requirements.
Further Reading:
Posted on August 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.