Latest Updates on React 18

Mar 13, 2022#react#react-18

This post is intended to summarize React 18 discussions on GitHub. At the time of writing React 18 has hit release candidate version. To try React 18 you need update to the latest React 18 release with the additional step of switching from ReactDOM.render to ReactDOM.createRoot.

npm install react@rc react-dom@rc
import * as ReactDOMClient from 'react-dom/client'
import App from './App'

const container = document.getElementById('app')
const root = ReactDOMClient.createRoot(container)
root.render(<App />)

React 18 includes out-of-the-box improvements to existing features. It is also the first React release to add support for Concurrent Features, which let you improve the user experience in ways that React didn’t allow before.

New Root API

In React, a root is a pointer to the top-level data structure that React uses to track a tree to render. When using legacy ReactDOM.render, the root was opaque to the user because we attached it to the DOM element, and accessed it through the DOM node, never exposing it to the user.

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

const container = document.getElementById('app')

// Initial render.
ReactDOM.render(<App tab="home" />, container)

// During an update, React would access
// the root of the DOM element.
ReactDOM.render(<App tab="profile" />, container)

React 18 introduces new Root API is called with ReactDOM.createRoot which adds all of the improvements of React 18 and allows you to use concurrent features.

import * as ReactDOMClient from 'react-dom/client'
import App from 'App'

const container = document.getElementById('app')

// Create a root.
const root = ReactDOMClient.createRoot(container)

// Initial render: Render an element to the root.
root.render(<App tab="home" />)

// During an update, there's no need to pass the container again.
root.render(<App tab="profile" />)

This change allows React to remove the hydrate method and replace with with an option on the root; and remove the render callback, which does not make sense in a world with partial hydration.

import * as ReactDOMClient from 'react-dom/client'

import App from 'App'

const container = document.getElementById('app')

// Create *and* render a root with hydration.
const root = ReactDOMClient.hydrateRoot(container, <App tab="home" />)
// Unlike with createRoot, you don't need a separate root.render() call here

Automatic Batching

Batching is when React groups multiple state updates into a single re-render for better performance because it avoids unnecessary re-renders.

However, React hasn’t been consistent about when it batches updates. React only batched updates during React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default.

React 18 does more batching by default, all updates will be automatically batched, no matter where they originate from.

function handleClick() {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React will only re-render once at the end (that's batching!)

But remember React only batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a key press, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice.

Concurrent Features

React 18 will add new features such as startTransition, useDeferredValue, concurrent Suspense semantics, SuspenseList, and more. To power these features, React added concepts such as cooperative multitasking, priority-based rendering, scheduling, and interruptions.

These features unlock new performance and user experience gains by more intelligently deciding when to render (or stop rendering) subtrees in an app.

  • startTransition: lets you keep the UI responsive during an expensive state transition.
  • useDeferredValue: lets you defer updating the less important parts of the screen.
  • <SuspenseList>: lets you coordinate the order in which the loading indicators appear.
  • Streaming SSR with selective hydration: lets your app load and become interactive faster.

Support Suspense in SSR

Suspense component lets you wait for some code to load and declaratively specify a loading state (like a spinner) while we’re waiting, but not available on the server.

One problem with SSR today is that it does not allow components to wait for data. With the current API, by the time you render to HTML, you must already have all the data ready for your components on the server.

React 18 offers two major features for SSR by using Suspense component. The improvements themselves are automatic inside React and we expect them to work with the majority of existing React code. This also means that React.lazy just works with SSR now.

  • Streaming HTML: lets you start emitting HTML as early as you’d like, streaming HTML for additional content together with the <script> tags that put them in the right places.
  • Selective Hydration: lets you start hydrating your app as early as possible, before the rest of the HTML and the JavaScript code are fully downloaded. It also prioritizes hydrating the parts the user is interacting with, creating an illusion of instant hydration.

There are different levels of support depending on which API you use:

  • renderToString: Keeps working (with limited Suspense support).
  • renderToNodeStream: Deprecated (with full Suspense support, but without streaming).
  • renderToPipeableStream: New and recommended (with full Suspense support and streaming).

Behavioral Changes to Suspense

React added basic support for Suspense since version 16 but it has been limited — it doesn’t support delayed transitions, placeholder throttling, SuspenseList.

Suspense works slightly differently in React 18 than in previous versions. Technically, this is a breaking change, but it won’t impose a significant migration burden on authors migrating their apps.

<Suspense fallback={<Loading />}>
  <ComponentThatSuspends />
  <Sibling />

The difference is how a suspended components affects the rendering behavior of its siblings:

  • Previously, the Sibling component is immediately mounted to the DOM and its effects/lifecycles are fired. Then React hides it.
  • In React 18, the Sibling component is not mounted to the DOM. Its effects/lifecycles are also NOT fired until ComponentThatSuspends resolves, too.

In previous versions of React, there was an implied guarantee that a component that starts rendering will always finish rendering.

In React 18, what React does instead is interrupt the siblings and prevent them from committing. React waits to commit everything inside the Suspense boundary — the suspended component and all its siblings — until the suspended data has resolved.