Concurrent mode and interruptible rendering
An experimental build of React was recently released that contained an exciting new feature, a way to fetch data and render UI elements with concurrency. This means we no longer have to fetch data in a useEffect() or componentDidMount() function and wait for the fetch to complete while the user is faced with a blank state screen before the elements all pop in together when the promises are resolved and the map function has mapped.
How does it work?
With the old method, which is considered a blocking render, as your app loads, it only loads the pieces it already has all the data ready for. Your navigation bars, backgrounds, and containers. What doesn't get loaded is the data inside the containers. The posts, pictures, avatars, and usernames. That is, until the necessary fetches complete and resolve and state is set. This is a problem because state, which updates asynchronously on the browser queue, can only set one item at a time. The order of events looks a lot like this:
- We request the users avatar and username
- We wait...
- We finish fetching the avatar and username
- We render the avatar and username components
- We start fetching the users posts and pictures
- We wait...
- We finish fetching the posts and pictures
- We render the posts and picture components
And to see it in action with a CodeSandbox from the React documentation (press the refresh button in the sandbox browser):
We can see that it first makes a request for the username, waits until it's complete while displaying "Loading profile...", waits until the username is loaded, displays it, and then begins fetching the posts. This is the blocking render. It renders things in the order in which they are received, and once it begins rendering and awaiting the data, it can't be stopped. Another example of blocking render is when typing in a dynamic search field. Say you have a search box for usernames, and after a user presses the "a" key, a state change is triggered, a fetch request for the usernames containing "a" is triggered, the results come in, the results are mapped to a collection of <SearchResult /> components, and they are displayed. Now that's a lot of steps... What happens when a user presses "b" halfway through? Well it triggers another state change... But state changes are asynchronous on the browser queue, so it has to wait for the fetch and render to finish before it can be changed, then we have to follow all of these steps again. This makes your input jumpy and laggy, especially with lots of results.
Suspense - How does it work?
Suspense takes a new approach by utilizing interruptible rendering, which is to say when new data is received, React determines the priority of the new addition to the queue and renders what it sees fit. In other words, if it is awaiting fetch requests to return data, in memory, it will build semi-constructed components and only render the pieces it has information for. So whenever a component has all the pieces it needs to be displayed, React pushes it to the top of the queue to be displayed next. The flow would look something like this:
- We start fetching
- We start rendering
- We finish fetching
Now that is a lot fewer steps and we never have to wait! We can again, see it in action here with a CodeSandbox from the React documentation (press the refresh button in the sandbox browser):
If we look closely, it begins rendering the username and the post components, then displays them quickly as they come in. Much faster! If we revisit our case of dynamic search and are looking for usernames again, what's the flow with Suspense? A user hits "a", a state change is triggered, Suspense starts building result components, a fetch reque... oh wait the user typed 'b'! Ok we still have the components being constructed, but they don't have any data yet, so let's just throw the new data on them! Boom! Rendered.
How to use it?
So how do we use this new amazing feature to enrich our applications and improve user experience by reducing jumpy loading and wait times? Here is a code snippet from the React docs:
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
The first thing we do is call a function that fires our fetch requests (in this case fetchProfileData()). Next in the return of our <ProfilePage /> functional component, we wrap both <ProfileDetails /> and <ProfileTimeline /> in <Suspense /> tags, and we provide it with a prop of "fallback". The fallback is simply what is loaded when the data isn't completely fetched yet and it can be an HTML element or another React component. The <Suspense /> magic behind the scenes starts building the <ProfileDetails /> and <ProfileTimeline /> components in memory and sees the <ProfileDetails /> component isn't complete yet, because the resource.user.read() function returned only a promise, so it displays the fallback element and it moves on and checks the resource.posts.read() return in the <ProfileTimeline /> component. This component isn't complete yet either, so it renders the fallback and moves back to the top of the list. Now the <ProfileDetails /> component is complete so it renders it, and checks <ProfileTimeline /> again which is now complete too, so it renders it as well. So use is really simple. We have a resource object that contains all of our data, a <Suspense /> tag that tells React it will be rendering concurrently, with a "fallback" function if the .read() function returns a promise instead of the response data.
Conclusion
Suspense and interruptible rendering are surprisingly easy to use and have a dramatic increase on load times, application speed, and user experience. Gone are the days of elements popping onto the screen unexpectedly and keyboard input being blocked by the render of elements. Gone are the days of keeping your users...... in suspense.
Top comments (1)
Fascinating stuff. How would this affect a setInterval loop that we would normally pair with a useEffect to avoid excessive re-renders? Or maybe it doesn't affect that?