React in version 17 is going to have important changes, adding two new features: React Suspense and Time Slicing. Today we’ll talk about the first one. Let’s look at everything you need to know about React Suspense.
There are two important factors that always come into play in every interface we build:
- The network speed our users have.
- The processing speed/capacity of the devices where our interfaces are used.
These two factors and how we address them lead us to make technical decisions in our products. With these two problems in mind, the React team has developed:
React Suspense, which gives us a standard way to request asynchronous data, focusing primarily on the user experience.
Time Slicing, which gives us a way to assign priority to changes in our application’s interface, resulting in better-performing applications.
Today we’re going to look in detail at how to use React Suspense. (If you want to see a demo of how both work, you can watch Dan Abramov’s talk at JSConf Iceland.)
All the features covered in this post are not yet ready for production use. There may still be changes before the final release.
What Is React Suspense?
React Suspense is the codename for the feature that gives us a standard way to load asynchronous data in our components.
The way it works, in a nutshell, is that when a component needs to perform an asynchronous action (like fetching data) before being displayed, React will “pause” the component’s render until the data is available.
To understand this, we need to look a bit at how component rendering works in React.
Every component’s render is divided into three phases:
Render Phase
In this phase, all the calculations for the differences between the virtual DOM and the DOM take place. This phase has no side effects, so it can be performed asynchronously. With the implementation of React Fiber, work can be paused, restored, or aborted during this phase (i.e., rendering a component).
Pre-commit Phase
In this phase, you can basically read the DOM, allowing for calculations.
Commit Phase
This phase is synchronous, since this is where the reconciliation process between what’s in the virtual DOM and the DOM takes place.
So, to make React Suspense possible, React takes control of the component by pausing it in the render phase until the data is resolved, without blocking the interface or causing performance issues.
The Current State of React
Before talking about how to implement React Suspense, I’d like to talk about how we fetch data today in React. What we normally do (if you’re not using a state manager) is use componentDidMount to fetch data.
We’d have an implementation that looks roughly like this:
class User extends Component {
state = {
users: []
}
componentDidMount() {
this.fetch()
}
fetch = async () => {
const users = await fetch("/users")
this.setState({ users })
}
render() {
const { users } = this.state
if(users.lenght === 0) {
return <Spinner />
}
return users.map(user => <User key={user.id} {...user} >)
}
}
Once the component mounts, the data request is made. In the meantime, an indicator (in this case a spinner) is shown to tell the user something is happening.
It’s very common to find this kind of implementation today, but it has some problems:
- If the user navigates away quickly and the promise hasn’t resolved yet, when it does resolve it’ll try to update the state of a component that no longer exists (React will show you a warning about this).
To fix this, you can use a library like Axios that lets you cancel promises, or use an architecture like Flux that lets you unsubscribe when unmounting a component to avoid this problem. But now we have another abstraction layer in our application.
- Users will always have to see the loading indicator, and this isn’t bad on slow connections since users get an indication that something is happening. But on fast connections, this indicator will be visible for less than a second, so we’ll have a bunch of indicators that barely flash on screen.
And there are some other problems that can be solved easily (or not), often adding complexity. So React Suspense has a pretty elegant solution for this.
How to Use React Suspense
To use React Suspense, we basically need three things:
- A resource
- A cache system
- Using Suspense
Let’s look at each one.
Creating a Resource with React Suspense
As I mentioned, React Suspense lets us pause a component’s render until the asynchronous data is resolved. To do this, we need to create a resource with the asynchronous data we need. We’ll use a small library called simple-cache-provider.
This library has a method called createResource that receives an asynchronous function and creates a resource.
import { createResource } from 'simple-cache-provider';
import { fetchUserDetails } from 'api'
const UserDetailsResource = createResource(fetchUserDetails)
When this resource is created, it has two methods:
.read— lets us execute the async function and read the value it resolves to..preload— lets us execute the async function without reading the value.
simple-cache-provideris a small library that primarily serves as a reference for other internal implementations in other libraries.
Cache in React Suspense
Now that we have our resource, it’s also important to be able to cache these (you know, caching is always a good thing). To create it, we can again use simple-cache-provider:
import { createCache } from 'simple-cache-provider';
const cache = createCache()
And just like that, we have a cache system.
Async Component in React Suspense
Now that we have our resource and cache, we can use them inside a component:
import { createResource, createCache } from 'simple-cache-provider';
import { fetchUserDetails } from 'api'
const cache = createCache()
const UserDetailsResource = createResource(fetchUserDetails)
function UserDetails({ id }) {
const user = UserDetailsResource.read(cache, id);
return <User {...user}/>
}
As you can see, we can use the resource inside the render of the component that needs the information. The read method takes the cache as its first argument, and the remaining arguments are passed to the async function.
This way we already have an async component that makes an API call, loads data, and displays it on screen. Now let’s see how to use this component in our interfaces.
The Suspense Component
Since our component is asynchronous and depends on the resource being resolved before it can display, we need a way to show a visual indicator if this loading takes too long. For this, React has a new component called Suspense.
We’ll need to wrap all our async components with Suspense (React throws a warning if you don’t):
import React, { Suspense } from 'react';
class App extends Component {
render() {
return (
<Suspense delayMs={500} fallback={<Spinner size="medium" />}>
<UserDetails id={this.props.id}/>
</Suspense>
)
}
}
Suspense receives two props: delayMs and fallback.
fallback is a component that should be shown if loading our resource doesn’t happen quickly, and delayMs is the time it should wait before showing this fallback.
We could tell it to wait 500ms. On fast connections, this fallback would never appear because the resource would load before that. But on slow connections, we’d have a loading indicator.
This also gives us more flexibility. Imagine that not all the endpoints you query are equally fast. If you know an endpoint takes a while to respond, you can show the fallback as soon as possible and let the fast one try to just display. Something like this:
import React, { Suspense, Fragment } from 'react';
import Spinner from 'spinner'
class App extends Component {
render() {
return (
<Fragment>
<Suspense delayMs={500} fallback={<Spinner size="medium" />}>
<UserDetails id={this.props.id}/>
</Suspense>
<Suspense delayMs={1} fallback={<Spinner size="medium" />}>
<UserComments id={this.props.id}/>
</Suspense>
</Fragment>
)
}
}
Suspense can wrap more than one async component, but in this case we want the time before showing the fallback to be different.
Nested Suspense
Async components can be inside other async components without any problem. So in our example, once the user details load, we could start loading their comments. To do this, we again need to use the Suspense component:
import React, { Suspense } from 'react';
import Spinner from 'spinner'
import { createCache, createResource } from 'simple-cache-provider';
import { fetchUserDetails } from 'api'
const cache = createCache()
const UserDetailsResource = createResource(fetchUserDetails)
function UserDetails({id}) {
const user = UserDetailsResource.read(cache, id);
return <div>
<h1>{user.name}</h1>
<Suspense delayMs={500} fallback={<Spinner size="medium" />}>
<UserComments userId={id} />
</Suspense>
</div>
}
This is pretty cool, but we depend on the user details resource finishing its load before the comments start loading. If we wanted to improve this, we can use the resource’s preload:
import React, { Suspense } from 'react';
import Spinner from 'spinner'
import { createCache, createResource } from 'simple-cache-provider';
import { fetchUserDetails, fetchCommets } from 'api'
const cache = createCache()
const UserDetailsResource = createResource(fetchUserDetails)
const UserCommentsResource = createResource(fetchCommets)
function UserDetails({id}) {
UserCommentsResource.preload(cache, id)
const user = UserDetailsResource.read(cache, id);
return <div>
<h1>{user.name}</h1>
<Suspense delayMs={500} fallback={<Spinner size="medium" />}>
<UserComments id={id} />
</Suspense>
</div>
}
This way, once the details resource starts loading, the comments will also start loading.
An important thing is that if the user navigates to another view, nothing bad happens. React won’t block the page or crash because the resources that are loading aren’t used.
Loading Resources with React Suspense
As I said, React Suspense is a standard way to load asynchronous resources, and this isn’t limited to just fetching data. We could load resources like images, videos, and audio using everything we’ve already seen.
Here’s a simple implementation of how to load an image:
import {createCache, createResource} from 'simple-cache-provider';
const cache = createCache()
const ImageResource = createResource(
src =>
new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(src);
img.src = src;
})
);
function Img({src, alt, ...rest}) {
return <img src={ImageResource.read(cache, src)} alt={alt} {...rest} />;
}
And to use it, you just need to wrap this Img component in Suspense:
function UserPicture({ source }) {
return (
<Suspense delayMs={1500} fallback={<img src={source} alt="poster" />}>
<Img src={source}/>
</Suspense>
)
);
This way we could load resources, and we could even preload them (remember preload). You can see how to implement loading for other resources in the react-async-elements codebase.
Code Splitting with React Suspense
If you were thinking that a component that’s required asynchronously should also work with this, you’re right. React has integrated a way to asynchronously require components, but for this we’ll use a new method called React.lazy.
React.lazy is simply an abstraction for creating a resource that lets us asynchronously require a component, using the import syntax:
import React, { Suspense } from 'react';
import Spinner from 'spinner';
import UserPage = React.lazy(() => import('../userPage'))
class App extends Component {
render() {
return (
<Suspense delayMs={500} fallback={<Spinner size="medium" />}>
<UserPage />
</Suspense>
)
}
}
You also need to use Suspense to handle cases where the component takes too long to load.
How to Use React Suspense Today
React Suspense is a feature still under construction, and there’s no prerelease you can install to try it out. To use it, we need to use the React repository, enable this feature, and build the project.
# clonar el repo
git clone [email protected]:facebook/react.git
# Entrar a la carpeta
cd react
# Instalar dependencias
yarn install
# Entrar al archivo ReactFeatureFlags.js
# En la línea 19, cambiar la variables enableSuspense por true
vim packages/shared/ReactFeatureFlags.js
# Construir React
yarn build dom,core,interaction,simple-cache-provider --type=NODE
Now we have our version of React built with React Suspense enabled. Inside the repository there’s also a small project built with Suspense that you can use to test the current API:
# Entrar al proyecto
cd fixtures/unstable-async/suspense/
# Instalar dependencias
yarn install
# Iniciar el proyecto
yarn start
That way you’ll have the project running locally. Throughout this post, we’ve covered all the things you’ll find in this project. Check out the project’s code — you’ll surely find interesting things.
Final Words
React Suspense is going to change the way we consume asynchronous resources in our applications, with an elegant solution and great flexibility. But more than anything, it’s going to change the experience for our users.
In the next post, we’ll talk about Time Slicing, the other big feature of React 17.