React 18 Concurrent Features Overview

Difficulty: Advanced

Question

What are the key concurrent features introduced in React 18?

Answer

React 18's concurrent features represent the biggest architectural shift in React since hooks were introduced in React 16.8. This isn't just a version bump with a few new APIs -- it's a fundamental change in how React renders components. Let me break down what changed and why it matters.

First, some background. Before React 18, rendering was synchronous. When React started rendering a component tree, it had to finish the entire thing before the browser could do anything else -- paint the screen, respond to user clicks, anything. If your component tree was large or your render logic was expensive, the browser would freeze for the duration of that render. The user clicks a button and nothing happens for 200ms because React is busy rendering a huge list. That's a bad experience.

Concurrent rendering changes this. React can now start rendering a component tree, pause in the middle to handle something more urgent (like a user clicking a button), and then come back and finish (or even abandon and restart) the render. It's like the difference between a single-lane highway and a multi-lane highway -- urgent traffic (user interactions) can take the fast lane while less urgent traffic (background data updates) takes the slow lane.

The gateway to all of this is createRoot. If you're still using the old ReactDOM.render(<App />, document.getElementById('root')), your app is running in legacy mode and none of the concurrent features are enabled. You need to switch to createRoot(document.getElementById('root')).render(<App />). This single change unlocks everything.

Let's go through the key features:

Automatic Batching. In React 17 and earlier, state updates were only batched inside React event handlers. If you had two setState calls inside an onClick, React would batch them into one render. But if those setState calls were inside a setTimeout, a fetch .then callback, or a native DOM event listener, each one would trigger a separate render. React 18 batches state updates everywhere -- in timeouts, promises, native events, anywhere. This is a free performance win that requires zero code changes.

useTransition and useDeferredValue are the developer-facing APIs for concurrent rendering. useTransition lets you mark a state update as low priority. When you call startTransition(() => setSearchResults(newResults)), React knows that updating the search results is less urgent than keeping the input responsive. If the user types another character while React is rendering the search results, React abandons the in-progress render and starts a new one with the updated input. The isPending flag from useTransition lets you show a loading indicator while the low-priority update is in progress.

useDeferredValue is similar but works on values rather than updates. You pass it a value and it returns a 'deferred' version that lags behind during urgent updates. It's useful when you can't control the state update (like a prop coming from a parent) but you want to defer the expensive rendering it causes.

Streaming SSR with Suspense is where things get really interesting for server-rendered apps. Traditional SSR has a waterfall problem: the server has to fetch ALL data, render ALL components to HTML, send ALL that HTML to the client, load ALL the JavaScript, and then hydrate ALL the components. Each step has to complete entirely before the next one can start.

React 18's streaming SSR breaks this pipeline. The server starts sending HTML immediately for the parts of the page that are ready. When it hits a Suspense boundary around a component that's still fetching data, it sends the fallback HTML and keeps streaming. When the data resolves, the server sends the completed HTML in a later chunk along with a small inline script that swaps it in. The user sees the page shell almost instantly, and content fills in progressively.

Selective Hydration takes this further on the client side. Instead of hydrating the entire page as one big chunk of work (which can freeze the browser), React hydrates each Suspense boundary independently. And here's the really clever part: if the user clicks on a component that hasn't been hydrated yet, React prioritizes hydrating that component first. The user doesn't have to wait for the entire page to become interactive -- they can interact with whichever part they click on.

React Server Components (RSCs) are the latest evolution, primarily used through Next.js App Router. RSCs are fundamentally different from SSR. With SSR, components run on the server to generate HTML, but then the same component code ships to the client and runs again during hydration. RSCs run only on the server. Their code never ships to the client. They never hydrate. They can directly access databases, file systems, and server-only APIs. The result is zero client-side JavaScript for those components -- they contribute nothing to your bundle size.

Client Components (marked with 'use client') work like traditional React components -- they run on both server and client, they hydrate, and they can use state and effects. The art of building a Next.js App Router application is deciding which components should be Server Components (most of them, especially data-fetching and layout components) and which need to be Client Components (interactive elements that need state, effects, or browser APIs).

All of these features are designed to solve the same fundamental problem: making React apps feel fast regardless of their complexity. Automatic batching reduces render count. useTransition keeps the UI responsive during expensive updates. Streaming SSR shows content earlier. Selective hydration makes the page interactive faster. RSCs reduce bundle size. They're all different angles on the same goal.

Code examples

createRoot and Concurrent Features

// React 17 (legacy mode)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// React 18 (concurrent mode)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// This single change enables:
// - Automatic batching
// - useTransition / useDeferredValue
// - Streaming SSR
// - Selective Hydration

// Streaming SSR with Suspense (Next.js App Router)
// The server streams HTML in chunks as data resolves
async function SlowComponent() {
  const data = await fetchSlowData(); // Server awaits this
  return <div>{data.title}</div>;
}

// Page structure - shell renders first, SlowComponent streams in
function Page() {
  return (
    <>
      <Header />  {/* Streams immediately */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />  {/* Streams when ready */}
      </Suspense>
      <Footer />  {/* Streams immediately */}
    </>
  );
}

createRoot is the gateway to all React 18 concurrent features. Streaming SSR with Suspense sends HTML progressively - users see the page shell before data loads.

Selective Hydration

// Selective Hydration: React prioritizes hydrating interactive parts first

// Server sends HTML:
// <Header /> (hydrated first when user interacts)
// <Suspense> <LazyContent /> </Suspense> (streamed when ready)
// <Footer /> (hydrated last)

// If user clicks on <Header> before hydration is complete:
// React prioritizes hydrating <Header> immediately
// Then continues with the rest

// Example: multiple Suspense boundaries
function App() {
  return (
    <>
      <Header /> {/* Hydrated first on interaction */}

      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar /> {/* Streamed and hydrated independently */}
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <MainContent /> {/* Streamed and hydrated independently */}
      </Suspense>

      <Footer />
    </>
  );
}

// Timeline:
// 1. Server sends Header + Footer HTML immediately
// 2. Sidebar and MainContent stream in as they resolve
// 3. React hydrates them independently (not as one big chunk)
// 4. User interaction on any part triggers immediate hydration of that part

Multiple Suspense boundaries allow independent streaming and hydration. React hydrates whichever part the user interacts with first.

Key points

Concepts covered

Concurrent Mode, React 18, createRoot, Streaming SSR, Selective Hydration