---
title: "React 19 in Next.js: Actions and the React version you run"
description: "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."
canonical_url: "https://www.outofplace.space/blog/react-19"
language: "en"
last_updated: "2026-11-05"
alternates:
  pl: "https://www.outofplace.space/pl/blog/react-19-w-nextjs-jak-uzywac.md"
---

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

Published 2026-11-05 · Category: Next.js & React · Tags: React, Next.js

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](https://www.outofplace.space/blog/react-server-components-explained).

Live demo: https://lab.outofplace.space/demos/react-19 (Next.js 16.4.3; source at commit f663557: https://github.com/outof-place/outofplace-lab/tree/f663557aa0c5bab3bad4e8d79bc41eaed40e2500/app/demos/react-19)

## 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](https://registry.npmjs.org/react) (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](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#react-18-3) for what 19 removes |
| 19.0.0 | 5 Dec 2024 | [Actions](https://react.dev/blog/2024/12/05/react-19#actions), `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](https://github.com/react/react/releases/tag/v19.1.0) |
| 19.2.0 | 1 Oct 2025 | [`<Activity>`, `useEffectEvent`, `cacheSignal`](https://react.dev/blog/2025/10/01/react-19-2), Performance Tracks, partial pre-rendering |
| 19.3.0 | 9 Sep 2026 | [Stable `<ViewTransition>`, Fragment Refs](https://react.dev/blog/2026/09/09/react-19-3), `browser()`, Trusted Types, `<Context>` in Server Components |
| 19.3.1 | 14 Oct 2026 | One change, [Improve transport protocol](https://github.com/react/react/releases/tag/v19.3.1) 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](https://github.com/react/react/security/advisories/GHSA-xqc2-xf4c-x3h3), 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](https://www.outofplace.space/blog/dev-digest-2026-10-18#react-1909-to-1931-a-critical-rce-in-server-components) and the Next.js fixes of the same day. React's [versions page](https://react.dev/versions) 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](https://nextjs.org/docs#react-version-handling) 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](https://nextjs.org/docs/app/guides/upgrading/version-15#react-19) 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](https://github.com/vercel/next.js/releases/tag/v16.4.0) name the build: "Upgrade React from `8b0da1c6-20260922` to `278794d7-20261002`". The notes of [16.4.1](https://github.com/vercel/next.js/releases/tag/v16.4.1), [16.4.2](https://github.com/vercel/next.js/releases/tag/v16.4.2) and [16.4.3](https://github.com/vercel/next.js/releases/tag/v16.4.3) list no React upgrade after it. On npm that build is [`react@19.3.0-canary-278794d7-20261002`](https://registry.npmjs.org/react/19.3.0-canary-278794d7-20261002); 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](https://react.dev/reference/rsc/server-functions) 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 (https://github.com/outof-place/outofplace-lab/blob/f663557aa0c5bab3bad4e8d79bc41eaed40e2500/app/demos/react-19/actions.ts)

```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 };
}
```

Used with [`useActionState`](https://react.dev/reference/react/useActionState#parameters), it receives the previous state first and the form data second, and its return value becomes the new state. The [Next.js forms guide](https://nextjs.org/docs/app/guides/forms#validation-errors) 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 (https://github.com/outof-place/outofplace-lab/blob/f663557aa0c5bab3bad4e8d79bc41eaed40e2500/app/demos/react-19/notes.tsx)

```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>;
}
```

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](https://react.dev/reference/react/useOptimistic#returns) 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](https://react.dev/reference/react-dom/hooks/useFormStatus#caveats) 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](https://react.dev/reference/react-dom/components/form#handle-form-submission-with-an-action-prop) 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 (https://github.com/outof-place/outofplace-lab/blob/f663557aa0c5bab3bad4e8d79bc41eaed40e2500/app/demos/react-19/page.tsx)

```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>
    </>
  );
}
```

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 (https://github.com/outof-place/outofplace-lab/blob/f663557aa0c5bab3bad4e8d79bc41eaed40e2500/app/demos/react-19/browser-version.tsx)

```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";
}
```

[`useSyncExternalStore` uses its server snapshot](https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering) (`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](https://nextjs.org/docs) name 16.4.5 as the latest version, the [Next.js 16.4.5 security release](https://www.outofplace.space/blog/nextjs-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](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#codemods).

**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](https://react.dev/reference/react-dom/hooks/useFormStatus#useformstatus-will-not-return-status-information-for-a-form-rendered-in-the-same-component). 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](https://react.dev/reference/react/useOptimistic#an-optimistic-state-update-occurred-outside-a-transition-or-action), 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](https://react.dev/reference/react-dom/components/form#display-a-form-submission-error-without-javascript) 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](https://nextjs.org/docs/app/guides/forms) 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](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis). 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.

## Further reading

- [React v19](https://react.dev/blog/2024/12/05/react-19): The release post of 5 December 2024: Actions, the new hooks, use, ref as a prop and metadata.

- [React 19 Upgrade Guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide): React 18.3 first, the codemods, and every removed and changed API.

- [React 19.3](https://react.dev/blog/2026/09/09/react-19-3): View Transitions and Fragment Refs become stable, plus browser() and Context in Server Components.

- [React: useActionState](https://react.dev/reference/react/useActionState): The hook's parameters, the previous-state argument and its caveats with Server Functions.

- [React: useOptimistic](https://react.dev/reference/react/useOptimistic): How optimistic state shows during an Action and why the setter must run inside one.

- [Next.js: How to create forms with Server Actions](https://nextjs.org/docs/app/guides/forms): Validation errors with useActionState, pending states and optimistic updates in the App Router.

- [Next.js: Upgrading to version 15](https://nextjs.org/docs/app/guides/upgrading/version-15): React 19 as the App Router's minimum and useFormState renamed to useActionState.

- [React: Unauthenticated Remote Code Execution in React Server Components](https://github.com/react/react/security/advisories/GHSA-xqc2-xf4c-x3h3): The advisory of 14 October 2026: affected packages and the patched 19.x versions.

- [React Server Components explained: what reaches the browser](https://www.outofplace.space/blog/react-server-components-explained): React 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.
