The road to React 19

React 19 hit a release candidate stage in April 2024, and the final stable version is expected to bring a wave of features that have been gestating since React 18. The headline additions focus on server-side rendering capabilities, a new suite of data-fetching hooks, and APIs aimed at streamlining resource loading.

Server Components become mainstream

React Server Components (RSC) are arguably the most significant architectural shift React has seen in a decade. They change the rendering model in three key ways:

  • Faster initial page loads. Server-side rendering cuts JavaScript sent to the browser and lets data queries start on the server before the user even receives the page.
  • Better code portability. Components can share logic between server and client environments, easing maintenance and reducing duplicated code.
  • Improved SEO. Server-rendered content is easier for search engines and LLMs to crawl.

The evolution from client-side rendering (CSR) to server-side rendering (SSR) was the first step. CSR shipped an essentially empty HTML shell to the user; React, third-party libraries, and app code all arrived as one large JavaScript bundle that needed downloading, parsing, and executing before the UI started appearing. Data fetched afterward forced an awkward second update pass.

<!DOCTYPE html>

<html>

<body>

<div id="root"></div>

<script src="/static/js/bundle.js"></script>

</body>

</html>

SSR moved the first render pass to the server, meaning the delivered HTML no longer appeared as a blank page. But fetching data to populate an SSR document still required an extra client-side request. React frameworks later built on that with SSG—caching dynamically rendered content at build time—and ISR, which refreshes that cache with dynamic data on demand.

Server Components remove that data-fetching round-trip from native React:

export default async function Page() {

const res = await fetch("https://api.example.com/products");

const products = res.json();

return (

<>

<h1>Products</h1>

{products.map((product) => (

<div key={product.id}>

<h2>{product.title}</h2>

<p>{product.description}</p>

</div>

))}

</>

);

}

The HTML that reaches the user already includes all the content. No second render cycle is triggered, nor does the browser need more data requests to realize the page immediately.

Directives: telling the bundler where code runs

With RSC, bundlers now have to know whether a component or function belongs on the server or the client. Two directives satisfy this requirement:

  • 'use client' designates code for the client only. Since Server Components are the default, client-side interactive features hooking into state or effects will need this directive.
  • 'use server' annotates server-side functions accessible from client code. This belongs on Server Actions (not Server Components themselves). The server-only npm package can further guard server code from leaking into client bundles.

Actions and Server Actions

React 19's Actions are a direct replacement for traditional event-handler-based submission flows, working alongside React transitions. They pass form data straight into the handler as FormData, skipping custom event parsing:

app.tsx

import { useState } from "react";

export default function TodoApp() {

const [items, setItems] = useState([

{ text: "My first todo" },

]);

async function formAction(formData) {

const newItem = formData.get("item");

// Could make a POST request to the server to save the new item

setItems((items) => [...items, { text: newItem }]);

}

return (

<>

<h1>Todo List</h1>

<form action={formAction}>

<input type="text" name="item" placeholder="Add todo..." />

<button type="submit">Add</button>

</form>

<ul>

{items.map((item, index) => (

<li key={index}>{item.text}</li>

))}

</ul>

</>

);

}

Server Actions bridge the gap further by letting a Client Component execute asynchronous functions on the server—including file-system reads or direct database calls—without a bespoke API endpoint. They are declared with the 'use server' directive in a dedicated module and imported by client code.

actions.ts

'use server'

export async function create() {

// Insert into database

}

todo-list.tsx

"use client";

import { create } from "./actions";

export default function TodoList() {

return (

<>

<h1>Todo List</h1>

<form action={create}>

<input type="text" name="item" placeholder="Add todo..." />

<button type="submit">Add</button>

</form>

</>

);

}

New hooks for form and UI state

React 19 bundles three hooks for working with reactive UI in forms and other interactions; all are usable anywhere the equivalent side-effect pattern would fit.

useActionState

This hook encapsulates submitted form input, validation and error logic into a single call through Actions. It also produces a pending state you can use to signal a request being processed. It is particularly useful without a full form workflow.

"use client";

import { useActionState } from "react";

import { createUser } from "./actions";

const initialState = {

message: "",

};

export function Signup() {

const [state, formAction, pending] = useActionState(createUser, initialState);

return (

<form action={formAction}>

<label htmlFor="email">Email</label>

<input type="text" id="email" name="email" required />

{/* ... */}

{state?.message && <p aria-live="polite">{state.message}</p>}

<button aria-disabled={pending} type="submit">

{pending ? "Submitting..." : "Sign up"}

</button>

</form>

);

}

useFormStatus

Call useFormStatus from a component nested within a form to track the status of the containing form's latest submission. It is appropriate for lighter scenarios where you only need status from:

  • components with a rendered status indicator and no dedicated form logic,
  • reusable form subcomponents,
  • or pages that house multiple forms, given it wires to whichever parent form.

import { useFormStatus } from "react-dom";

import action from "./actions";

function Submit() {

const status = useFormStatus();

return <button disabled={status.pending}>Submit</button>;

}

export default function App() {

return (

<form action={action}>

<Submit />

</form>

);

}

useOptimistic

The hook updates UI state optimistically before a background Server Action resolves. You declare an initial state and a patch function; while the async action runs, useOptimistic immediately renders the change. When complete, it synchronises with the server's returned final state.

"use client";

import { useOptimistic } from "react";

import { send } from "./actions";

export function Thread({ messages }) {

const [optimisticMessages, addOptimisticMessage] = useOptimistic(

messages,

(state, newMessage) => [...state, { message: newMessage }],

);

const formAction = async (formData) => {

const message = formData.get("message") as string;

addOptimisticMessage(message);

await send(message);

};

return (

<div>

{optimisticMessages.map((m, i) => (

<div key={i}>{m.message}</div>

))}

<form action={formAction}>

<input type="text" name="message" />

<button type="submit">Send</button>

</form>

</div>

);

}

New API: use

The use function adds first-class support for promising resources and context during render, and is the only hook that tolerates being placed inside conditions and loops. While a promise is unresolved, React isolates it to the nearest Suspense boundary. Components that each await a data source can then render only once all their results are ready.

import { use } from "react";

function Cart({ cartPromise }) {

// `use` will suspend until the promise resolves

const cart = use(cartPromise);

return cart.map((item) => <p key={item.id}>{item.title}</p>);

}

function Page({ cartPromise }) {

return (

/*{ ... }*/

// When `use` suspends in Cart, this Suspense boundary will be shown

<Suspense fallback={<div>Loading...</div>}>

<Cart cartPromise={cartPromise} />

</Suspense>

);

}

Resource preloading APIs

Six new DOM utilities handle async resource management ahead of the time a component needs them:

  • prefetchDNS — resolves a domain's IP ahead of a connection.
  • preconnect — opens a connection to a server you'll call imminently.
  • preload — fetches an asset such as a stylesheet, font, image, or external script that will be needed.
  • preloadModule — fetches a pending ESM module.
  • preinit — fetches and evaluates an external script, or loads and inlines a stylesheet link.
  • preinitModule — evaluates a particular ESM module early.

// React code

import { prefetchDNS, preconnect, preload, preinit } from "react-dom";

function MyComponent() {

preinit("https://.../path/to/some/script.js", { as: "script" });

preload("https://.../path/to/some/font.woff", { as: "font" });

preload("https://.../path/to/some/stylesheet.css", { as: "style" });

prefetchDNS("https://...");

preconnect("https://...");

}

<!-- Resulting HTML -->

<html>

<head>

<link rel="prefetch-dns" href="https://..." />

<link rel="preconnect" href="https://..." />

<link rel="preload" as="font" href="https://.../path/to/some/font.woff" />

<link

rel="preload"

as="style"

href="https://.../path/to/some/stylesheet.css"

/>

<script async="" src="https://.../path/to/some/script.js"></script>

</head>

<body>

<!-- ... -->

</body>

</html>

Notably, React applies a strict order priority to render these tags in HTML rather than preserving the sequence in which components call them. In practice, frameworks will often manage these behind the scenes, but direct access remains there for custom integrations.

Refs, Context, and Cleanup

React 19 removes the need for forwardRef entirely—ref can now be passed as a prop directly to function components. A codemod is provided to help migrate existing code. Refs also gained cleanup support: a ref callback can return a cleanup function, which React invokes when the component unmounts.

<input

ref={(ref) => {

// ref created

// Return a cleanup function to reset

// ref when element is removed from DOM.

return () => {

// ref cleanup

};

}}

/>;

Context usage is similarly simplified. <Context.Provider> is no longer required; you can render <Context> directly as the provider, with a codemod available to convert existing providers.

const ThemeContext = createContext("");

function App({ children }) {

return <ThemeContext value="dark">{children}</ThemeContext>;

}

The useDeferredValue hook now accepts an initialValue option. When supplied, the hook uses it for the initial render and schedules a background re-render to return the deferred value.

function Search({ deferredValue }) {

// On initial render the value is ''.

// Then a re-render is scheduled with the deferredValue.

const value = useDeferredValue(deferredValue, "");

return <Results value={value} />;

}

Document, Stylesheet, and Script Handling

React 19 natively hoists title, link, and meta tags rendered from nested components to the document <head>, eliminating the need for third-party metadata management libraries. Stylesheets can be colocated with components, with a precedence prop controlling loading order. React loads such stylesheets only when the component using them is rendered, and deduplicates them if the same component appears multiple times.

Key behaviors to note:

  • Server-side rendering includes the stylesheet in <head>, preventing paint until it loads.
  • After streaming starts, React inserts newly discovered stylesheets into the <head> on the client before revealing dependent content via Suspense boundaries.
  • During client-side rendering, React waits for newly rendered stylesheets to load before committing the render.

function ComponentOne() {

return (

<Suspense fallback="loading...">

<link rel="stylesheet" href="one" precedence="default" />

<link rel="stylesheet" href="two" precedence="high" />

<article>...</article>

</Suspense>

);

}

function ComponentTwo() {

return (

<div>

<p>...</p>

{/* Stylesheet "three" below will be inserted between "one" and "two" */}

<link rel="stylesheet" href="three" precedence="default" />

</div>

);

}

Async scripts also support colocation. React deduplicates repeated script renders and, during server-side rendering, prioritizes async scripts behind critical paint-blocking resources such as stylesheets, fonts, and image preloads.

function Component() {

return (

<div>

<script async={true} src="..." />

// ...

</div>

);

}

function App() {

return (

<html>

<body>

<Component>

// ...

</Component> // Won't duplicate script in the DOM

</body>

</html>

);

}

Custom Elements and Errors

Full support for Custom Elements arrives in React 19. Previous versions treated unrecognized props as attributes rather than properties, making the Web Components API difficult to use. React 19 passes all tests on Custom Elements Everywhere, removing that friction.

Error reporting got a cleanup pass. React no longer logs duplicate errors when a component fails and then fails again during recovery; the error is displayed once. Hydration mismatch errors are now logged once instead of multiple times, with messages that include guidance on fixing the cause. Unexpected tags inserted by third-party scripts or browser extensions are skipped over silently, rather than triggering hydration mismatches.

Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error. Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error. In React 19, the error is only displayed once.

Two new root options supplement the existing onRecoverableError:

  • onCaughtError fires when an Error Boundary catches an error.
  • onUncaughtError fires when an error is thrown without being caught by a boundary.
  • onRecoverableError fires when an error is thrown and automatically recovered.
Example of a hydration error message in React 18.

Starting with React 19

With production-ready support for the framework’s new features, React 19 is available now across major toolchains. Ready-made deployment templates let you try React 19 with Astro, Next.js 15 RC, Vite, and Waku.