React 19 in Next.js: Actions and the React version you run
React 19 is stable since 5 Dec 2024 and 19.3.1 is the latest. A live Next.js 16 demo of Actions, and why the App Router runs its own React, not package.json's.

React 19 has been stable since 5 December 2024, and the latest release is 19.3.1, from 14 October 2026. To use it in Next.js, take the App Router on Next.js 15 or later: install next@latest react@latest react-dom@latest, then write forms as React 19 Actions, a "use server" function behind useActionState, with useFormStatus for the pending button and useOptimistic for instant feedback. One thing surprises most teams: the App Router does not run the React in your package.json. Our demo's package.json asks for react@19.3.1, but it runs Next.js 16.4.3, whose built-in React is the canary 19.3.0-canary-278794d7-20261002 according to the Next.js release notes, and the demo prints the React version that renders. We checked every date against the npm registry and every API against the React and Next.js docs on 5 November 2026; seven Playwright tests check what the demo does. For what runs where between those components, see React Server Components explained.
React 19 release date and versions
React 19 is a major version with minor releases on top of it. The dates below come from the npm registry (opens in a new tab) (UTC), read on 5 November 2026:
| Version | On npm | What it brought |
|---|---|---|
| 18.3.1 | Apr 2024 | The last React 18: 18.2 plus deprecation warnings (opens in a new tab) for what 19 removes |
| 19.0.0 | 5 Dec 2024 | Actions (opens in a new tab), useActionState, useFormStatus, useOptimistic, use, ref as a prop, document metadata, stable Server Components |
| 19.1.0 | 28 Mar 2025 | Owner Stack, Suspense and hydration fixes (opens in a new tab) |
| 19.2.0 | 1 Oct 2025 | <Activity>, useEffectEvent, cacheSignal (opens in a new tab), Performance Tracks, partial pre-rendering |
| 19.3.0 | 9 Sep 2026 | Stable <ViewTransition>, Fragment Refs (opens in a new tab), browser(), Trusted Types, <Context> in Server Components |
| 19.3.1 | 14 Oct 2026 | One change, Improve transport protocol (opens in a new tab) for Server Components, the version the advisory below lists as patched |
19.3.1 is npm's latest tag. It patches the critical advisory Unauthenticated Remote Code Execution in React Server Components (opens in a new tab), published the same day, which covers react-server-dom-webpack, -turbopack and -parcel from 19.0.0 to 19.3.0, with fixes in 19.0.9, 19.1.10, 19.2.9 and 19.3.1; our digest of 18 October explains the critical RCE in React Server Components and the Next.js fixes of the same day. React's versions page (opens in a new tab) still names 19.3 as the latest version and lists 19.3.0 as its newest entry, so pages that copy it are a release behind. For version questions, npm is the source to trust.
Does Next.js support React 19?
Yes, and the router decides which React you get. The Next.js docs on React version handling (opens in a new tab) say the App Router uses React canary releases built into Next.js, while the Pages Router uses the version installed in your package.json. The Next.js 15 upgrade guide (opens in a new tab) made 19 the minimum version of react and react-dom.
| App Router | Pages Router | |
|---|---|---|
| React that runs | The React canary built into Next.js | The react and react-dom you install |
| To change the React that runs | Upgrade next | Upgrade react and react-dom |
The demo lets you see which one renders. Its Server Component prints version from react as the server renders the page, and a Client Component prints it again once the browser's React has hydrated, while the lab's package.json asks for react@19.3.1, as the comment in page.tsx says. The Next.js 16.4.0 release notes (opens in a new tab) name the build: "Upgrade React from 8b0da1c6-20260922 to 278794d7-20261002". The notes of 16.4.1 (opens in a new tab), 16.4.2 (opens in a new tab) and 16.4.3 (opens in a new tab) list no React upgrade after it. On npm that build is react@19.3.0-canary-278794d7-20261002 (opens in a new tab); its version string carries the date 2 October 2026, twelve days before 19.3.1. You still install react and react-dom, as the install command in the Next.js upgrade guides does, but in the demo the version in package.json is not the one that renders.
How React 19 Actions work in the demo
An Action, in React 19's terms, is an async function run inside a transition, so React can track its pending state, its errors and its optimistic updates for you. Pass one to a <form>'s action prop and React runs it in a transition with the form's data. A Server Function (opens in a new tab) is an async function marked "use server" that a Client Component calls and the server runs; passed to an action prop or called inside an action, React's docs call it a Server Action, the name Next.js uses.
The Server Function
The demo's Server Function waits 1.5 seconds, standing in for a slow database write, and returns the new list:
"use server"; export type Note = { id: string; text: string; savedAt: string };export type NotesState = { notes: Note[]; error: string | null }; // A Server Function for useActionState: it gets the previous state first, then the form's// data, and what it returns becomes the new state in the browser. `previous` comes from the// browser, so a real app would read and write its notes in a database instead.export async function addNote(previous: NotesState, formData: FormData): Promise<NotesState> { const text = String(formData.get("note") ?? "").trim(); if (!text) return { ...previous, error: "Write a note first." }; // Stands in for a slow write, so the pending and optimistic states stay on screen. await new Promise((resolve) => setTimeout(resolve, 1500)); const note = { id: crypto.randomUUID(), text: text.slice(0, 40), savedAt: new Date().toISOString() }; return { notes: [note, ...previous.notes].slice(0, 3), error: null };}Used with useActionState (opens in a new tab), it receives the previous state first and the form data second, and its return value becomes the new state. The Next.js forms guide (opens in a new tab) puts it the same way: with useActionState the Server Function's signature changes to take the previous state as its first argument. An empty note returns early with an error in that state, which is how validation errors reach the form without a thrown exception.
The form: useActionState, useOptimistic and useFormStatus
"use client"; import { useActionState, useOptimistic } from "react";import { useFormStatus } from "react-dom"; import { addNote, type Note, type NotesState } from "./actions";import styles from "./react-19.module.css"; const initialState: NotesState = { notes: [], error: null }; type Shown = Note | { id: string; text: string; savedAt: null }; export function Notes() { // useActionState keeps what the last action returned: the notes the server saved, or an error. const [state, formAction] = useActionState(async (previous: NotesState, formData: FormData) => { const text = String(formData.get("note") ?? "").trim(); if (text) addOptimistic(text); return addNote(previous, formData); }, initialState); // While the action runs, the list shows the new note too; when it ends, React drops the // optimistic note and renders state.notes, which now holds the saved one. const [notes, addOptimistic] = useOptimistic<Shown[], string>(state.notes, (current, text) => [ { id: `sending-${current.length}`, text, savedAt: null }, ...current, ]); return ( <section className={styles.notes} aria-label="Notes"> <form action={formAction} className={styles.form}> <input name="note" aria-label="Note" placeholder="Write a note" maxLength={40} autoComplete="off" /> <SubmitButton /> </form> {state.error && ( <p role="alert" className={styles.error}> {state.error} </p> )} <ul className={styles.list}> {notes.map((note) => ( <li key={note.id} data-state={note.savedAt ? "saved" : "sending"}> <span>{note.text}</span> {note.savedAt ? ( <time dateTime={note.savedAt}>saved {note.savedAt.slice(11, 19)} UTC</time> ) : ( <em>sending…</em> )} </li> ))} </ul> </section> );} // useFormStatus reads the <form> around it, so it has to sit in a child component.function SubmitButton() { const { pending } = useFormStatus(); return <button disabled={pending}>{pending ? "Adding…" : "Add"}</button>;}Three hooks share the work, and each one is visible in the demo:
useOptimisticputs the note in the list at once, marked sending…, while the server is still waiting. The optimistic state (opens in a new tab) equals the value you pass unless an Action is pending, so React drops the temporary note on its own when the action ends.useFormStatusturns the button into a disabled Adding… while the action runs, and back to Add afterwards. The useFormStatus caveats (opens in a new tab) say it reads only the parent<form>, which is why it lives in a child component.useActionStateholds what the Server Function returned: the saved notes, each with the server's time, or for a blank note the error Write a note first. with the list unchanged. The next valid note returnserror: null, which clears the message.
The form has no reset code. The <form> reference (opens in a new tab) says React resets a form's uncontrolled fields when the action function succeeds, and the note input here is uncontrolled.
The page and the version lines
import { version } from "react"; import { BrowserReactVersion } from "./browser-version";import { Notes } from "./notes";import styles from "./react-19.module.css"; // A Server Component, so `version` is the React that renders on the server. The lab's// package.json asks for react 19.3.1; the App Router uses the React build that ships inside Next.js.export default function Page() { return ( <> <dl className={styles.versions}> <div> <dt>Server</dt> <dd> <output aria-label="React on the server">{version}</output> </dd> </div> <div> <dt>Browser</dt> <dd> <output aria-label="React in the browser"> <BrowserReactVersion /> </output> </dd> </div> </dl> <Notes /> <p className="hint"> Add a note. It shows up at once as sending…, the button reads Adding… while the Server Function runs (it waits 1.5 s), and the note turns saved, with the server's time, when the answer arrives. </p> </> );}The page is a Server Component, so its version is the React that renders on the server. The browser's line comes from a small Client Component:
"use client"; import { useSyncExternalStore, version } from "react"; const subscribe = () => () => {}; // The server snapshot (null) is used on the server and during hydration, so the version shows// only once the React that runs in the browser renders this component.export function BrowserReactVersion() { const browser = useSyncExternalStore(subscribe, () => version, () => null); return browser ?? "not hydrated yet";}useSyncExternalStore uses its server snapshot (opens in a new tab) (null here) on the server and during hydration, then renders again with the browser's value, so the line shows a version only once the browser's React runs the component. Copy the two files into your own app to see which React it runs.
How to use React 19 in a Next.js app
Upgrade Next.js and React together
Run
npm install next@latest react@latest react-dom@latest, plus@types/andreact @types/with TypeScript. On 5 November 2026 the Next.js docs (opens in a new tab) name 16.4.5 as the latest version, the Next.js 16.4.5 security release; the demo runs on 16.4.3.react-dom Come from React 18 through 18.3
On a React 18 app, upgrade to React 18.3 first, fix its warnings, then run
npx codemod@latest react/from React's upgrade guide (opens in a new tab).19/ migration-recipe Check which React runs
Print
versionfromreactin a Server Component and in a Client Component, as the demo does. In the App Router both show the canary build inside your Next.js version.Write the Server Function
Put an async function in a file that starts with
"use server". WithuseActionStateit takes the previous state and the form data, and returns the next state, errors included.Wire the form with useActionState
In a Client Component, call
useActionState(action, initialState)and pass the function it returns to<form action>. Show errors and results from the state.Add useFormStatus and useOptimistic
Put the submit button in its own component that calls
useFormStatus(). Call theuseOptimisticsetter inside the action, before you await the server.Test a production build
Run
next buildandnext startand submit the form: the pending, optimistic and saved states should each appear.
What breaks React 19 Actions
useFormStatus in the same component as the form
useFormStatus reads only a parent <form>. Call it in the component that renders the form and React's docs warn that pending will never be true (opens in a new tab). Move the button into a child component, as SubmitButton in the demo.
An optimistic update outside an Action
Call the useOptimistic setter in a plain event handler and the optimistic state briefly appears and then reverts (opens in a new tab), because no transition holds it. Call it inside the function you pass to action, or inside startTransition.
Wrapping the Server Function costs the no-JavaScript path
React's form docs on errors without JavaScript (opens in a new tab) say it can show a Server Function's returned value before JavaScript loads only when a Client Component renders the form, the function in its action is the Server Function itself, and useActionState shows the result. The demo wraps addNote in a client function so it can call the optimistic setter first, and gives that up. For a sign-up or contact form that must work before hydration, pass the Server Function to useActionState directly and skip useOptimistic.
Trusting what the browser sends
The previous state that useActionState hands your Server Function comes from the browser, like the form data. A Server Function is a public endpoint: the forms guide asks you to verify authentication and authorisation (opens in a new tab) inside each Server Action, even on a page only signed-in users see. Read real data from your database, not from the state.
Old names and removed APIs
useFormState became useActionState, and the Next.js 15 upgrade guide notes the old hook is deprecated in React 19. React 19 also removed ReactDOM.render, ReactDOM.hydrate, findDOMNode, string refs, propTypes and defaultProps on function components, legacy context and react-dom/, listed in the React 19 upgrade guide (opens in a new tab). App Router code rarely uses them; older libraries and tests often do.
Patching react alone
On the App Router, bumping react in package.json does not change the React that renders your pages: the Next.js docs say the App Router uses the canary built into Next.js. The October advisory asks you to upgrade the affected react-server-dom-* packages immediately. The App Router uses the React built into Next.js, so keep next on its latest release as well as react and react-dom, which the Pages Router runs.
When was React 19 released?
React 19.0.0 reached npm on 5 December 2024, about two years and eight months after React 18.0.0 (29 March 2022). Minor releases followed: 19.1.0 on 28 March 2025, 19.2.0 on 1 October 2025 and 19.3.0 on 9 September 2026.
What is the latest version of React?
19.3.1, published on npm on 14 October 2026 and tagged latest there, as of 5 November 2026. It fixes a critical remote code execution advisory in the React Server Components packages. React's own versions page still lists 19.3.0 as the newest entry, so go by npm.
Does Next.js support React 19?
Yes. Since Next.js 15 the minimum version of react and react-dom is 19, and the App Router runs a React canary build that ships inside Next.js rather than the react package in your package.json. Next.js 16.4.3 runs 19.3.0-canary-278794d7-20261002. The Pages Router uses the React version you install.
How do I upgrade to React 19?
From React 18, upgrade to 18.3 first: it is 18.2 plus warnings for what React 19 removes. Then run npx codemod@latest react/19/migration-recipe and install react and react-dom 19 with their TypeScript types. In a Next.js app, install next@latest with them, since the App Router uses the React build bundled with Next.js.
Is the React Compiler part of React 19?
No. React Compiler is a separate build-time tool that memoises components for you, and it supports React 17, 18 and 19. In Next.js you install babel-plugin-react-compiler and set reactCompiler: true in next.config.ts, a top-level option since Next.js 16.0.0.
