In this post, we will discuss the concept of concurrent rendering in React 18 and the new features that depend on this new mechanism.
React 18 was released in beta in November with new features and out-of-the-box improvements to already-existing features. This React release supports what is called Concurrent Features, which allow you to improve your user experience in new and exciting ways.
In the context of React.js, concurrency refers to having more than one task in progress at once, and concurrent tasks can overlap depending on which is more urgent. For example, while I write this post, I am preparing Jollof rice (a Nigerian delicacy). When the time comes to add ingredients to the meal, that will be more urgent, so I’ll pause writing and attend to that and come back to continue writing when I’m done. Meanwhile, my food will still be cooking. At different points throughout the writing and cooking processes, my focus will be on what is more urgent.
React could only handle one task at a time in the past, and a task could not be interrupted once it had started. This approach is referred to as Blocking Rendering. To fix this problem, Concurrent Mode was introduced—which makes rendering interruptible.
React gets easier when you have an expert by your side. KendoReact is a professional UI component library on a mission to help you design & build business apps with React much faster. Check it out!
Concurrent Mode was introduced as an experimental feature. In favor of a more gradual adoption plan that allows you to opt in to concurrent rendering at your own pace, Concurrent Mode is now being replaced in React 18 with concurrent features.
Concurrent rendering describes how the new features (concurrent features) included in React 18 are implemented. With concurrent rendering, you can improve your app’s performance by declaring some state updates as non-urgent to keep the browser responsive. It will be automatically enabled in the specific parts of your app that use the new concurrent features because concurrent features were built on top of concurrent rendering.
The startTransition
API introduced with React 18 helps you keep your app responsive without blocking your user interactions by allowing you to mark specific updates as transitions.
There are two categories of state updates in React:
React considers state updates wrapped in startTransition
as non-urgent, so they can be suspended or interrupted by urgent updates.
For example, as a user, it would feel more natural to see the letters as you type in a search input field for filtering data, but as expected, the search result may take a while, and that’s OK.
import { startTransition } from 'react';
// Urgent
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition
setSearchQuery(input);
})
In React, all updates are handled as urgent by default, but in React 18, you can mark an update as a transition by wrapping it in a StartTransition
API, as seen above. (You can learn more about the StartTransition
hook in this article.)
React can also track and update pending state transitions using the useTransition
hook with an isPending
flag. This lets you display loading feedback to your users, letting them know that work is happening in the background.
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
{isPending && <Spinner />}
This API keeps the UI responsive by telling React to defer updating the parts of a screen that take too long. For example, if we have a part of a UI that renders immediately and another part that needs to take some time, we can defer the part that requires more time by showing its old value while other components update.
useDefferedValue
takes in a state value and a timeout in milliseconds and returns the deferred version of that state value. The timeout tells React how long it should delay the deferred value.
import { useState, useDeferredValue } from "react";
function App() {
const [input, setInput] = useState("");
const deferredValue = useDeferredValue(text, { timeoutMs: 3000 });
return (
<div>
<input value={input} onChange={handleChange} />
<MyList text={deferredValue} />
</div>
);
}
With what we have in the code snippet above, the input value will be displayed immediately when a user starts typing, but useDeferredValue
will display a previous version of the MyList
component for at most 3000 milliseconds.
Server-side rendering is a technique that allows us to generate HTML from React components on the server and then send a fully rendered HTML page to the client. Your users won’t be able to interact with the HTML, but it will provide them with content to see before your JavaScript bundle loads. After the HTML is rendered in the browser, the React and JavaScript code for the entire app starts loading. When it is done, it connects the JavaScript logic to the server-generated HTML—a process known as hydration.
When the hydration process for the whole page is complete, your users can fully interact with your application. In the previous versions of React, hydration could only begin after the entire data had been fetched from the server and rendered to HTML. Additionally, your users couldn’t interact with the page until hydration was complete for the whole page. The problem with this approach is that for every step, the parts of your application that load fast will always have to wait for the parts that are slow to finish loading, and this isn’t very efficient.
To solve this problem, the Suspense component was released in 2018. The only supported use case was lazy-loading code on the client, not during server rendering. But React 18 added support for Suspense on the server. React 18 offers two major features for SSR unlocked by Suspense:
You can wrap a part of the page with Suspense
and provide a fallback prop. Suspense is a component that allows us to specify what should happen when code for another component isn’t ready or is taking some time. This instructs React not to wait for that component to load but instead continue streaming HTML for the rest of the page. React will display the component you provided as a fallback to your users while waiting for the component to load.
<Layout>
<Article />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</Layout>
In the code snippet above, we wrapped the Comments
component in a Suspense boundary and provided a fallback prop (Spinner) to be displayed. React will go ahead and render the Article
, and when the HTML for the Comments
becomes available on the server, it will be added to the same stream along with a script tag and inserted in the right place.
As seen above, wrapping our Comments
component in Suspense
tells React not to block the streaming of the HTML for our Article
component from the server. It also tells React not to block hydration by waiting for all the JavaScript code to load. This means that React can start hydrating the rest of the page, and when the HTML for the Comments
section is ready, it’ll get hydrated.
<Layout>
<Article />
<Suspense fallback={<Loader />}>
<Comments />
<Card />
</Suspense>
</Layout>
Another cool improvement added to Suspense is that if a user interacts with a component wrapped in Suspense
whose HTML has been loaded but is yet to be hydrated, React will prioritize hydrating the parts that the user interacted with before hydrating the rest of the app. See here for more on the Suspense SSR architecture for React 18.
First, you need to upgrade your React and ReactDOM npm packages to React 18, and then you also need to move from ReactDOM.render
to ReactDOM.createRoot
. After this, you automatically opt into concurrent rendering in the parts of your app that use the concurrent features.
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Create a root
const root = ReactDOM.createRoot(container);
// Initial render
root.render(<App />);
This new root API adds out-of-the-box improvements and gives us access to the concurrent features.
In this post, we discussed the concept of concurrent rendering in React 18 and the new features—concurrent features—that depend on this new mechanism, which we have access to by default when we opt into concurrency.