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.

Reading time: 7 min readPublished:

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.

Add a note: it appears at once marked sending…, the button reads Adding… for 1.5 seconds, then the note turns saved with the server's time. The two lines above the form name the React build the App Router really runs. · Next.js 16.4.3Open the demoView source

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:

VersionOn npmWhat it brought
18.3.1Apr 2024The last React 18: 18.2 plus deprecation warnings (opens in a new tab) for what 19 removes
19.0.05 Dec 2024Actions (opens in a new tab), useActionState, useFormStatus, useOptimistic, use, ref as a prop, document metadata, stable Server Components
19.1.028 Mar 2025Owner Stack, Suspense and hydration fixes (opens in a new tab)
19.2.01 Oct 2025<Activity>, useEffectEvent, cacheSignal (opens in a new tab), Performance Tracks, partial pre-rendering
19.3.09 Sep 2026Stable <ViewTransition>, Fragment Refs (opens in a new tab), browser(), Trusted Types, <Context> in Server Components
19.3.114 Oct 2026One 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 RouterPages Router
React that runsThe React canary built into Next.jsThe react and react-dom you install
To change the React that runsUpgrade nextUpgrade 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:

app/demos/react-19/actions.ts
"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 };}
From the lab at commit f663557

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

app/demos/react-19/notes.tsx
"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>;}
From the lab at commit f663557

Three hooks share the work, and each one is visible in the demo:

  • useOptimistic puts 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.
  • useFormStatus turns 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.
  • useActionState holds 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 returns error: 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

app/demos/react-19/page.tsx
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&apos;s time, when the answer arrives.      </p>    </>  );}
From the lab at commit f663557

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:

app/demos/react-19/browser-version.tsx
"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";}
From the lab at commit f663557

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

  1. Upgrade Next.js and React together

    Run npm install next@latest react@latest react-dom@latest, plus @types/react and @types/react-dom 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.

  2. 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/19/migration-recipe from React's upgrade guide (opens in a new tab).

  3. Check which React runs

    Print version from react in 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.

  4. Write the Server Function

    Put an async function in a file that starts with "use server". With useActionState it takes the previous state and the form data, and returns the next state, errors included.

  5. 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.

  6. Add useFormStatus and useOptimistic

    Put the submit button in its own component that calls useFormStatus(). Call the useOptimistic setter inside the action, before you await the server.

  7. Test a production build

    Run next build and next start and 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/test-utils, 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.

Further reading

React v19 (opens in a new tab)The release post of 5 December 2024: Actions, the new hooks, use, ref as a prop and metadata.react.dev
React 19 Upgrade Guide (opens in a new tab)React 18.3 first, the codemods, and every removed and changed API.react.dev
React 19.3 (opens in a new tab)View Transitions and Fragment Refs become stable, plus browser() and Context in Server Components.react.dev
React: useActionState (opens in a new tab)The hook's parameters, the previous-state argument and its caveats with Server Functions.react.dev
React: useOptimistic (opens in a new tab)How optimistic state shows during an Action and why the setter must run inside one.react.dev
Next.js: How to create forms with Server Actions (opens in a new tab)Validation errors with useActionState, pending states and optimistic updates in the App Router.nextjs.org
Next.js: Upgrading to version 15 (opens in a new tab)React 19 as the App Router's minimum and useFormState renamed to useActionState.nextjs.org
React: Unauthenticated Remote Code Execution in React Server Components (opens in a new tab)The advisory of 14 October 2026: affected packages and the patched 19.x versions.github.com
Next.js & ReactReact Server Components explained: what reaches the browserReact Server Components in Next.js 16, in a live demo: what stays on the server, what ships as JavaScript, and what the HTML and RSC payload carry.

Tell us what we’re building.

We reply within one business day.