Next.js 15 and React 19: What's New and How to Leverage It in Your Projects

I have upgraded several production Next.js apps to version 15 and React 19, including a marketing platform that serves thousands of users daily. Some of those upgrades were smooth. Some were not, and the pain had nothing to do with the features everyone writes about.
So this is not a release-notes recap. It is what actually changed in Next.js 15 and React 19, what broke for me, and where the new features earn their place in a real codebase. If you are deciding whether to upgrade, or you already started and hit a wall, this should save you a weekend.
The Short Version
React 19 is mostly about making forms, data mutations, and async UI states less painful. Next.js 15 is mostly about fixing developer experience complaints and making caching less surprising.
| Area | React 19 | Next.js 15 |
|---|---|---|
| Forms and mutations | Actions, useActionState, useFormStatus |
Server Actions are stable and the default path |
| Async data in components | use() hook for promises and context |
Async params, searchParams, cookies(), headers() |
| Optimistic UI | useOptimistic |
Works directly with Server Actions |
| Caching defaults | Not applicable | fetch no longer cached by default, GET route handlers uncached |
| Metadata and head | Native <title>, <meta>, <link> support |
Better streaming metadata handling |
| Build and DX | React Compiler (opt-in) | Turbopack dev stable, typed routes, better error overlays |
That table is the whole article in miniature. Now let me explain the parts that matter.
React 19 Features Worth Actually Using
Actions Replace a Lot of Boilerplate
Before React 19, a simple form submission meant a useState for loading, one for errors, one for the response, and a try/catch around a fetch call. Now you can pass an async function straight to a form. You can read the full details in the React 19 release notes.
// app/contact/ContactForm.tsx
"use client";
import { useActionState } from "react";
import { submitContact } from "./actions";
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, {
ok: false,
message: "",
});
return (
<form action={formAction} className="space-y-4">
<input name="email" type="email" required />
<textarea name="message" required />
<button disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}
The server side is just a function with "use server" at the top. No API route, no manual JSON parsing, no loading state juggling. I have replaced entire API layers with this pattern and the codebase got noticeably smaller. If you want the full walkthrough of wiring these up to real forms and data mutations, my Next.js 15 Server Actions tutorial covers it end to end.
One caveat I learned the hard way: useActionState returns a third value, isPending, and it is the one people forget. If you build your own pending state with useState, you are fighting the framework for no reason.
The use() Hook Changes How You Read Async Data
use() lets you read a promise or context inside a component. In Next.js, the most practical use is reading context without the ceremony of useContext in places where conditional reads matter. The React use() documentation covers the promise and context cases in detail.
"use client";
import { use, Suspense } from "react";
import { ThemeContext } from "./theme-context";
function ThemedLabel() {
const theme = use(ThemeContext);
return <span className={`text-${theme}-fg`}>Current theme: {theme}</span>;
}
export function Header() {
return (
<Suspense fallback={<span>Loading...</span>}>
<ThemedLabel />
</Suspense>
);
}
The important detail: a component that calls use() on a promise must be wrapped in Suspense. Skip that and you get an error that is less obvious than it should be.
useOptimistic for UI That Feels Instant
This is the hook I reach for most on client projects. A like button, a todo toggle, a comment post. The user sees the change immediately while the server catches up.
"use client";
import { useOptimistic, startTransition } from "react";
import { toggleLike } from "./actions";
export function LikeButton({ postId, likes }: { postId: string; likes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(current, delta: number) => current + delta
);
return (
<button
onClick={() =>
startTransition(async () => {
addOptimisticLike(1);
await toggleLike(postId);
})
}
>
{optimisticLikes} likes
</button>
);
}
Two rules I follow: the optimistic update must happen inside a transition, and the reducer must be pure. Break either one and you get flickering counts that are very annoying to debug.
Native Document Metadata
You can now render <title>, <meta>, and <link> tags directly in components. React hoists them to <head>. In Next.js you will still mostly use the Metadata API, but this matters for component libraries that ship their own head tags.
The React Compiler
The compiler is the quiet giant here. It auto-memoizes components and hooks, which means fewer useMemo and useCallback calls scattered through your code. In my experience it is not a magic performance switch. It removes a class of re-render bugs and lets you delete defensive memoization. Note that the compiler is still an opt-in tool rather than a default part of the React 19 runtime, see the React Compiler docs for current status.
Enable it and measure. Do not assume it makes a slow app fast. If your problem is a 400ms database query, the compiler will not help you.
Next.js 15 Improvements That Change Daily Work
Async Request APIs
This is the breaking change that catches almost everyone. params, searchParams, cookies(), and headers() are now async. The Next.js 15 upgrade guide documents the full set of changes.
// Before
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// Next.js 15
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
The codemod handles most of this, but it does not handle every pattern, especially when params get passed down into helper functions. Search your codebase for params. and searchParams. after running it. I found several files the codemod missed in one project.
Caching Is No Longer Silent
In Next.js 14, fetch requests were cached by default and route handlers were static by default. That caused a lot of "why is my data stale" support tickets. Next.js 15 flips both. Nothing is cached unless you ask. The Next.js caching documentation explains the new defaults.
// Explicitly opt in to caching
const res = await fetch("https://api.example.com/posts", {
cache: "force-cache",
});
// Revalidate on an interval
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
I think this is the single best change in the release. Explicit caching is easier to reason about than implicit caching, even if it means writing a few more lines.
Server Actions Are the Default Mutation Path
Combined with React 19 Actions, Server Actions now feel like the intended way to mutate data, not a beta feature. Progressive enhancement works: forms submit even before hydration finishes. If you are still deciding between server and client rendering for these flows, my breakdown of React Server Components vs Client Components in Next.js 15 is a useful companion read.
Turbopack for Development
next dev --turbopack is stable. On a mid-size app I saw dev server startup drop from around 12 seconds to under 3. Hot reload feels close to instant. For production builds I still use the standard bundler on most projects, though Turbopack builds have been fine on smaller apps. Check the Next.js Turbopack docs for the current stability status, since this is evolving quickly.
Better Error Messages and Typed Routes
Next.js 15 improved the error overlay with clearer stack traces that point at your code instead of internal framework files. Typed routes catch broken Link hrefs at build time, which has saved me from shipping at least two 404s.
A Practical Migration Checklist
Here is the order I use. It has worked on several apps and I have not had to roll one back.
- Upgrade React and React DOM to 19 first, on its own branch. Fix type errors before touching Next.js.
- Run the Next.js codemod:
npx @next/codemod@canary upgrade latest. - Convert every page and layout that reads
params,searchParams,cookies(), orheaders()to async. - Audit caching. Add explicit
cacheorrevalidateoptions where you relied on the old defaults. - Check third-party libraries. Anything that touches React internals, older UI kits, or animation libraries is the usual suspect.
- Run the build, not just the dev server. Several issues only appear in production builds.
- Test forms end to end, especially anything with optimistic updates.
A quick note on step 5. Peer dependency conflicts are the number one reason upgrades stall. If npm or pnpm complains, do not reach for --force immediately. Check whether the maintainer has a React 19 release, and if not, ask yourself whether you still need that library.
Where These Two Releases Actually Pay Off
If you are building a content site, the wins are modest. Faster dev server, clearer errors, slightly better caching semantics. Nice, not transformative.
If you are building anything with forms, dashboards, or data mutation, the wins are large. I rewrote a multi-step onboarding flow using Actions and useActionState, and it deleted a few hundred lines of state management code. That is the kind of change that pays rent.
For client projects, I now default to Next.js 15 and React 19 on anything new. For existing apps, I upgrade when the app has a clear mutation-heavy surface, and I wait when it depends on libraries that have not caught up. If you are staffing this work rather than doing it yourself, my guide on how to hire a Next.js developer in 2026 covers the skills and red flags to screen for.
FAQ
Q: Do I have to upgrade React and Next.js at the same time?
A: No, but the order matters. Upgrade React to 19 first and fix type errors, then move Next.js to 15. Doing both at once makes it hard to tell which change broke something.
Q: Is the App Router required in Next.js 15?
A: No. The Pages Router still works. That said, almost every new feature in this release (Server Actions, async params, streaming metadata) assumes the App Router. If you are on Pages Router and planning new work, this is a reasonable moment to migrate.
Q: What is the most common mistake when upgrading?
A: Forgetting that params and searchParams are now promises. The codemod catches most cases, but not when you pass params into helper functions or spread them into other objects. Grep for those patterns after running it.
Q: Should I enable the React Compiler right away?
A: On a new project, yes. On an existing one, enable it on a branch, run your test suite, and compare bundle and render metrics. It is stable enough to use, but it changes memoization behavior, and you want to see that diff before it hits production.
If you are planning a Next.js 15 or React 19 migration and would rather not spend a weekend on peer dependency conflicts, I do this regularly for teams in Bangladesh, the USA, Australia, Canada, and New Zealand. Reach out through the contact page and tell me what stack you are on.
Frequently asked questions
Do I have to upgrade React and Next.js at the same time?
No, but the order matters. Upgrade React to 19 first and fix type errors, then move Next.js to 15. Doing both at once makes it hard to tell which change broke something.
Is the App Router required in Next.js 15?
No. The Pages Router still works. That said, almost every new feature in this release (Server Actions, async params, streaming metadata) assumes the App Router. If you are on Pages Router and planning new work, this is a reasonable moment to migrate.
What is the most common mistake when upgrading?
Forgetting that params and searchParams are now promises. The codemod catches most cases, but not when you pass params into helper functions or spread them into other objects. Grep for those patterns after running it.
Should I enable the React Compiler right away?
On a new project, yes. On an existing one, enable it on a branch, run your test suite, and compare bundle and render metrics. It is stable enough to use, but it changes memoization behavior, and you want to see that diff before it hits production.