Concurrent UI Patterns (Experimental)
Caution:
This page is somewhat outdated and only exists for historical purposes.
React 18 was released with support for concurrency. However, there is no “mode” anymore, and the new behavior is fully opt-in and only enabled when you use the new features.
For up-to-date high-level information, refer to:
For details about concurrent APIs in React 18, refer to:
React.Suspense
referenceReact.startTransition
referenceReact.useTransition
referenceReact.useDeferredValue
referenceThe rest of this page includes content that’s stale, broken, or incorrect.
Usually, when we update the state, we expect to see changes on the screen immediately. This makes sense because we want to keep our app responsive to user input. However, there are cases where we might prefer to defer an update from appearing on the screen.
For example, if we switch from one page to another, and none of the code or data for the next screen has loaded yet, it might be frustrating to immediately see a blank page with a loading indicator. We might prefer to stay longer on the previous screen. Implementing this pattern has historically been difficult in React. Concurrent Mode offers a new set of tools to do that.
Transitions
Let’s revisit this demo from the previous page about Suspense for Data Fetching.
When we click the “Next” button to switch the active profile, the existing page data immediately disappears, and we see the loading indicator for the whole page again. We can call this an “undesirable” loading state. It would be nice if we could “skip” it and wait for some content to load before transitioning to the new screen.
React offers a new built-in useTransition()
Hook to help with this.
We can use it in three steps.
First, we’ll make sure that we’re actually using Concurrent Mode. We’ll talk more about adopting Concurrent Mode later, but for now it’s sufficient to know that we need to use ReactDOM.createRoot()
rather than ReactDOM.render()
for this feature to work:
const rootElement = document.getElementById("root");
// Opt into Concurrent Mode
ReactDOM.createRoot(rootElement).render(<App />);
Next, we’ll add an import for the useTransition
Hook from React:
import React, { useState, useTransition, Suspense } from "react";
Finally, we’ll use it inside the App
component:
function App() {
const [resource, setResource] = useState(initialResource);
const [isPending, startTransition] = useTransition(); // ...
By itself, this code doesn’t do anything yet. We will need to use this Hook’s return values to set up our state transition. There are two values returned from useTransition
:
isPending
is a boolean. It’s React telling us whether that transition is ongoing at the moment.startTransition
is a function. We’ll use it to tell React which state update we want to defer.
We will use them right below.
Caution:
In earlier experimental releases and demos, the order of the return values was reversed.
Wrapping setState in a Transition
Our “Next” button click handler sets the state that switches the current profile in the state:
<button
onClick={() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId)); }}
>
We’ll wrap that state update into startTransition
. That’s how we tell React we don’t mind React delaying that state update if it leads to an undesirable loading state:
<button
onClick={() => {
startTransition(() => { const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
}); }}
>
Press “Next” a few times. Notice it already feels very different. Instead of immediately seeing an empty screen on click, we now keep seeing the previous page for a while. When the data has loaded, React transitions us to the new screen.
React only “waits” for <Suspense>
boundaries that are already displayed. If you mount a new <Suspense>
boundary as a part of transition, React will display its fallback immediately.
Caution:
In earlier experimental releases and demos, there was a configurable timeout. It was removed.
Adding a Pending Indicator
There’s still something that feels broken about our last example. Sure, it’s nice not to see a “bad” loading state. But having no indication of progress at all feels even worse! When we click “Next”, nothing happens and it feels like the app is broken.
Our useTransition()
call returns two values: startTransition
and isPending
.
const [isPending, startTransition] = useTransition();
We’ve already used startTransition
to wrap the state update. Now we’re going to use isPending
too. React gives this boolean to us so we can tell whether we’re currently waiting for this transition to finish. We’ll use it to indicate that something is happening:
return (
<>
<button
disabled={isPending} onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
Next
</button>
{isPending ? " Loading..." : null} <ProfilePage resource={resource} />
</>
);
Now, this feels a lot better! When we click Next, it gets disabled because clicking it multiple times doesn’t make sense. And the new “Loading…” tells the user that the app didn’t freeze.
Reviewing the Changes
Let’s take another look at all the changes we’ve made since the original example:
function App() {
const [resource, setResource] = useState(initialResource);
const [isPending, startTransition] = useTransition(); return (
<>
<button
disabled={isPending}
onClick={() => {
startTransition(() => { const nextUserId = getNextId(resource.userId); setResource(fetchProfileData(nextUserId)); }); }}
>
Next
</button>
{isPending ? " Loading..." : null} <ProfilePage resource={resource} />
</>
);
}
It took us only seven lines of code to add this transition:
- We’ve imported the
useTransition
Hook and used it in the component that updates the state. - We’ve wrapped our state update into
startTransition
to tell React it’s okay to delay it. - We’re using
isPending
to communicate the state transition progress to the user and to disable the button.
As a result, clicking “Next” doesn’t perform an immediate state transition to an “undesirable” loading state, but instead stays on the previous screen and communicates progress there.
Where Does the Update Happen?
This wasn’t very difficult to implement. However, if you start thinking about how this could possibly work, it might become a little mindbending. If we set the state, how come we don’t see the result right away? Where is the next <ProfilePage>
rendering?
Clearly, both “versions” of <ProfilePage>
exist at the same time. We know the old one exists because we see it on the screen and even display a progress indicator on it. And we know the new version also exists somewhere, because it’s the one that we’re waiting for!
But how can two versions of the same component exist at the same time?
This gets at the root of what Concurrent Mode is. We’ve previously said it’s a bit like React working on state update on a “branch”. Another way we can conceptualize is that wrapping a state update in startTransition
begins rendering it “in a different universe”, much like in science fiction movies. We don’t “see” that universe directly — but we can get a signal from it that tells us something is happening (isPending
). When the update is ready, our “universes” merge back together, and we see the result on the screen!
Play a bit more with the demo, and try to imagine it happening.
Of course, two versions of the tree rendering at the same time is an illusion, just like the idea that all programs run on your computer at the same time is an illusion. An operating system switches between different applications very fast. Similarly, React can switch between the version of the tree you see on the screen and the version that it’s “preparing” to show next.
An API like useTransition
lets you focus on the desired user experience, and not think about the mechanics of how it’s implemented. Still, it can be a helpful metaphor to imagine that updates wrapped in startTransition
happen “on a branch” or “in a different world”.
Transitions Are Everywhere
As we learned from the Suspense walkthrough, any component can “suspend” any time if some data it needs is not ready yet. We can strategically place <Suspense>
boundaries in different parts of the tree to handle this, but it won’t always be enough.
Let’s get back to our first Suspense demo where there was just one profile. Currently, it fetches the data only once. We’ll add a “Refresh” button to check for server updates.
Our first attempt might look like this:
const initialResource = fetchUserAndPosts();
function ProfilePage() {
const [resource, setResource] = useState(initialResource);
function handleRefreshClick() { setResource(fetchUserAndPosts()); }
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<button onClick={handleRefreshClick}> Refresh </button> <Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
In this example, we start data fetching at the load and every time you press “Refresh”. We put the result of calling fetchUserAndPosts()
into state so that components below can start reading the new data from the request we just kicked off.
We can see in this example that pressing “Refresh” works. The <ProfileDetails>
and <ProfileTimeline>
components receive a new resource
prop that represents the fresh data, they “suspend” because we don’t have a response yet, and we see the fallbacks. When the response loads, we can see the updated posts (our fake API adds them every 3 seconds).
However, the experience feels really jarring. We were browsing a page, but it got replaced by a loading state right as we were interacting with it. It’s disorienting. Just like before, to avoid showing an undesirable loading state, we can wrap the state update in a transition:
function ProfilePage() {
const [isPending, startTransition] = useTransition(); const [resource, setResource] = useState(initialResource);
function handleRefreshClick() {
startTransition(() => { setResource(fetchProfileData()); }); }
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<button
onClick={handleRefreshClick}
disabled={isPending}
>
{isPending ? "Refreshing..." : "Refresh"} </button>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
This feels a lot better! Clicking “Refresh” doesn’t pull us away from the page we’re browsing anymore. We see something is loading “inline”, and when the data is ready, it’s displayed.
The Three Steps
By now we have discussed all of the different visual states that an update may go through. In this section, we will give them names and talk about the progression between them.