Skip to content

React 18: Concurrency and Streaming SSR

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

React 18

The React team just announced React 18 alpha, along with their plans for release, earlier this month. Included in that announcement is the creation of a working group to help prepare the community for the new features, nearly all of which can be gradually adopted.

Concurrency (Concurrent mode begone!)

So we finally have an answer to what we've been wondering about for the past three years! React 18 will ship with a new concurrency model, but not a completely separate concurrency "mode". Concurrency will come from a new set of features, and just happen automatically when those features are used, so it is completely optional.

createRoot will now be the way that we initialize the root of our React applications going forward as you can see here:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

const root = ReactDOM.createRoot(container);

root.render(<App />);

This new createRoot also enables all the new concurrent features- a subtle distinction from a "mode" 😉. This model allows you to render work and events to be interleaved together, and allows you to assign priority to various kinds of updates. This gives us better ways of keeping UI fluid and performant.

Priority

To achieve concurrency, React now uses a cooperative multitasking model which runs a single thread that can be interrupted based on priority. Interruptions happen in between rendering various components. Since the time it takes to render a component is usually very small, it provides a very granular way to interrupt when high priority work comes through.

If you'd like to read more in depth, check out this topic in the working group.

startTransition

One of the new concurrent features is startTransition, which allows us to specify updates that might be lower priority- aka "transition" updates. Specifying updates as "transition", enables urgent updates to interrupt them, allowing interactions to be more performant.

To do this, we'll use a new API:

import { startTransition } from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

Urgent vs Transition Updates

Urgent actions are things like clicking, hovering, scrolling, typing, or any other action where the user expect things to happen instantly as they do natively in the browser. Transition updates are more "React-centric" things like handling and managing UI state.

Currently, all updates in React 18 and below are considered urgent. The way this is worded seems to imply a future where all updates are considered transitions, and we only need to specify urgent updates. I think it was done this way to make the adoption even more gradual, because I think realistically, most updates should be transitional, not urgent. However, people need time to get used to the idea of specifying priority before everything actually gets de-prioritized.

Automatic Update Batching

A small but significant change is that now all updates are automatically batched. You may have noticed, when building React apps, that any time you put multiple setState calls next to each other in something like an event handler, React combines them into one update so that only one render occurs. However, you may have also noticed that this does not occur in things like callbacks, setTimeout, Promises, etc.

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handlePrevClick() {
	  setCount(c => c - 1);
	  setFlag(f => !f);
	  //Currently, React will only re-render once at the end (batching!)
  } 

  function handleNextClick() {
    fetchSomething().then(() => {
      // React 18 and later DOES batch these:
      setCount(c => c + 1);
      setFlag(f => !f);
      // React will only re-render once at the end (that's batching!)
    });
  }

  return (
    <div>
	    <button onClick={handlePrevClick}>Prev</button>
      <button onClick={handleNextClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

flushSync

However, maybe you were relying upon that non-batching behavior! Well, there's a new API to opt-out of batching (although the team says this should be uncommon):

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

Streaming Server Render

Probably one of the most impactful changes in this set is now the ability for server-side rendering or SSR to be streamed to the browser, enabling partial page sections to be rendered before the entire page is ready. This should speed up the time it takes for the user to see anything on the page.

Streaming + Selective Hydration

What does this mean exactly? So first, current SSR efforts do the entire render of the HTML page, and wait until it's completely finished before sending it to the browser. The new method allows the page to be broken up into chunks using <Suspense>. Now, the server can start streaming HTML as soon as one of these chunks is ready.

Even if that HTML gets to the browser faster, it still needs to be hydrated. Previously, this was an all-or-nothing operation, requiring the entire page's JavaScript to be downloaded. Now, we can use a combination of <Suspense> and React.lazy() to achieve partial hydration as well.

If you'd like to see an example of how the server handles this, the React team put together a nice demo:

Event Replay and Priority

If that wasn't enough already, the team also found a way to prioritize hydration for user interactive sections. React will now record interactions with various parts of the page, and prioritize hydration based on what has been interacted with. This is really elegant because it lets things that the user cares about render as quickly as possible. 👏👏👏👏

Suspense is Now Very Important

These new features are all enabled entirely by the <Suspense> component. Wrapping parts of the page that may take longer to load enables their execution to be deferred while the rest of the page gets rendered, streamed, and hydrated, enabling things to be faster and quicker to interact with!

If you'd like to see an in-depth explanation, check out this topic in the working group complete with diagrams.

Conclusion

This is an extremely exciting update with lots of promise towards better performing apps and faster load times! Increased control over what gets rendered and hydrated first will lead to much better user experiences, especially on devices that struggle to load JS efficiently.

I'm very glad we will finally get an answer on concurrency "mode" too! 😂

If you're excited to try these new changes, go ahead and install the alpha:

npm install react@alpha react-dom@alpha  

The React team is looking for feedback through the working group, so feel free to participate in discussions there.

I have to give lots of credit towards @swyx, who did a lot of investigation work to point out key features from the release in this thread. Check it out!

Thanks!

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co