We want to hear from you!Take our 2021 Community Survey!
This documentation is for React 17.Go to the latest docs.

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:

The 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));
    });  }}
>

Try it on CodeSandbox

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} />
  </>
);

Try it on CodeSandbox

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} />
    </>
  );
}

Try it on CodeSandbox

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>
  );
}

Try it on CodeSandbox

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>
  );
}

Try it on CodeSandbox

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.