What Is React 19?
React 19 is the first major version since React 18 and it rewires how you think about async state. Instead of wiring up useEffect + useState + error boundaries manually, React 19 gives you first-class primitives for the entire async lifecycle.
React Actions
Actions are async functions that React knows about. When you pass them to <form action={fn}> or use them with the new hooks, React handles pending state, errors, and optimistic updates automatically.
import { useActionState } from "react";
async function submitForm(prevState: State, formData: FormData) {
const result = await saveToServer(formData.get("name") as string);
if (!result.ok) return { error: "Save failed" };
return { success: true };
}
function Form() {
const [state, action, isPending] = useActionState(submitForm, null);
return (
<form action={action}>
<input name="name" disabled={isPending} />
{state?.error && <p>{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save"}
</button>
</form>
);
}
The use() Hook
use() is the escape hatch that lets you read a Promise or Context inside the render function. Unlike other hooks, it can be called inside loops and conditionals.
import { use, Suspense } from "react";
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends while pending
return <div>{user.name}</div>;
}
// In the parent:
<Suspense fallback={<Skeleton />}>
<UserCard userPromise={fetchUser(id)} />
</Suspense>
Reading context with use() is useful inside conditional branches:
if (needsTheme) {
const theme = use(ThemeContext); // valid — hooks rules relaxed for use()
}
useOptimistic()
Show the result of a mutation instantly while the server request is in flight:
import { useOptimistic, useTransition } from "react";
function TaskList({ tasks }: { tasks: Task[] }) {
const [optimisticTasks, addOptimistic] = useOptimistic(
tasks,
(state, newTask: Task) => [...state, newTask]
);
const [, startTransition] = useTransition();
function handleAdd(title: string) {
const newTask = { id: crypto.randomUUID(), title };
startTransition(async () => {
addOptimistic(newTask); // UI updates immediately
await createTask(title); // server catches up
});
}
return <ul>{optimisticTasks.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
Refs as Props — No More forwardRef
In React 19, functional components accept ref as a regular prop. The forwardRef wrapper is no longer needed (and will be deprecated in a future minor):
// Before (React 18)
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input {...props} ref={ref} />
));
// After (React 19)
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input {...props} ref={ref} />;
}
Server Actions Are Stable
Server Actions work in both Server and Client Components. In a Client Component, import a Server Action and call it like a regular async function:
"use client";
import { deleteTask } from "@/app/actions";
function DeleteButton({ id }: { id: string }) {
return (
<button onClick={() => deleteTask(id)}>Delete</button>
);
}
The React Compiler (Formerly Forget)
The React Compiler — previously called React Forget — is in beta for React 19. It automatically memoizes components and values, eliminating the need for manual useMemo, useCallback, and memo() in most cases.
Enable it in Next.js 15:
// next.config.ts
export default {
experimental: {
reactCompiler: true,
},
};
Migration Steps
pnpm add react@19 react-dom@19- Replace
useFormStatewithuseActionState - Remove
forwardRef— passrefas a prop directly - Replace manual optimistic state patterns with
useOptimistic() - Run the React 19 codemod:
npx codemod@latest react/19/migration-recipe
References: React 19 blog · migration guide