
Next.js 15 and React 19 are here — and they change more than you think.
Introduction
React 19 and Next.js 15 landed together and represent the most significant shift in the React ecosystem since hooks. Together they introduce the React Compiler, a matured Server Components model, Server Actions as a first-class primitive, and a completely rethought caching strategy that fixes some of the most complained-about pain points in Next.js 13 and 14.
If you've been building with the App Router and wondered why caching felt confusing or why you kept reaching for "use client" more than expected — this release addresses exactly that.
This post walks through every major change with practical examples you can apply to a real project today.
1. React 19 — What Actually Changed
React 19 is a major version, but the surface-area changes are focused. The team spent the cycle on three things: the compiler, new hooks, and stabilising the async/server primitives.
The React Compiler
The biggest change is one you mostly won't see — the React Compiler (previously React Forget). It's a build-time compiler that automatically inserts memoisation so you no longer need to manually write useMemo, useCallback, or React.memo for performance.
// Before React 19 — you had to do this manually
const expensiveValue = useMemo(() => computeHeavyThing(input), [input]);
const stableCallback = useCallback(() => doSomething(id), [id]);
// After React 19 Compiler — just write normal code
// The compiler handles memoisation automatically
const expensiveValue = computeHeavyThing(input);
const handleClick = () => doSomething(id);
The compiler analyses your component graph and inserts the right optimisations at build time. You don't install it separately — it ships with React 19 and is enabled automatically when you upgrade.
Important: The compiler only works correctly if your code follows the Rules of React (no mutations of props/state, no side effects in render). If your codebase has violations, the compiler will skip those components and fall back to the previous behaviour rather than break things.
New use() Hook
use() is a new primitive that lets you unwrap a Promise or a Context inside a component — including inside conditionals, which no other hook allows.
import { use } from "react";
import { Suspense } from "react";
// You can pass a Promise directly and React suspends until it resolves
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends here until resolved
return <h2>{user.name}</h2>;
}
// Wrap with Suspense as usual
function Page() {
const userPromise = fetchUser("abc123");
return (
<Suspense fallback={<p>Loading...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
use() also replaces useContext() in most cases and can be called conditionally:
// ✅ This is valid in React 19 — hooks in conditionals are now allowed for `use()`
function ThemeButton({ showTheme }: { showTheme: boolean }) {
if (showTheme) {
const theme = use(ThemeContext); // ✅ conditional use() is fine
return <button style={{ background: theme.primary }}>Click</button>;
}
return <button>Click</button>;
}
Actions — Async Transitions
React 19 formalises the concept of Actions — async functions that manage pending state, errors, and optimistic updates.
import { useActionState } from "react";
async function updateUsername(prevState: State, formData: FormData) {
const username = formData.get("username") as string;
const result = await saveUsername(username);
if (!result.ok) return { error: "Failed to save", username };
return { error: null, username };
}
function UsernameForm() {
const [state, action, isPending] = useActionState(updateUsername, {
error: null,
username: "",
});
return (
<form action={action}>
<input name="username" defaultValue={state.username} />
{state.error && <p className="text-red-500">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Saving…" : "Save"}
</button>
</form>
);
}
useOptimistic — Stable
useOptimistic is now stable and the recommended way to show immediate UI feedback before an async action completes:
import { useOptimistic } from "react";
function MessageList({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, newMessage]
);
async function sendMessage(text: string) {
// Immediately show the message
addOptimistic({ id: "temp", text, status: "sending" });
// Then actually send it
await postMessage(text);
}
return (
<ul>
{optimisticMessages.map((m) => (
<li key={m.id} className={m.status === "sending" ? "opacity-50" : ""}>
{m.text}
</li>
))}
</ul>
);
}
🚀 2. Next.js 15 — The Key Changes
Caching Is Now Opt-In (Breaking Change)
This is the most important behavioural change in Next.js 15. In versions 13 and 14, fetch() calls were cached by default and you had to opt out with { cache: 'no-store' }. This caused widespread confusion.
In Next.js 15, all fetches are uncached by default. You now opt in to caching explicitly:
// Next.js 13/14 — cached by default, had to opt out
const data = await fetch("/api/posts"); // cached ❌ confusing
const fresh = await fetch("/api/posts", { cache: "no-store" }); // not cached
// Next.js 15 — uncached by default, opt in to cache ✅ clear
const data = await fetch("/api/posts"); // NOT cached — fresh every request
const cached = await fetch("/api/posts", {
next: { revalidate: 3600 }, // cached for 1 hour
});
const staticData = await fetch("/api/posts", {
cache: "force-cache", // cached indefinitely until revalidated
});
The same applies to Route Handlers and Client Router Cache — both now default to no-store.
Async Request APIs
cookies(), headers(), params, and searchParams are now async in Next.js 15. This was needed to support streaming and partial prerendering properly.
// Next.js 14 — synchronous
import { cookies, headers } from "next/headers";
export default function Page({ params }: { params: { slug: string } }) {
const cookieStore = cookies(); // sync
const token = cookieStore.get("token");
// ...
}
// Next.js 15 — async ✅
import { cookies, headers } from "next/headers";
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // await params
const cookieStore = await cookies(); // await cookies
const token = cookieStore.get("token");
// ...
}
Migration tip: Next.js 15 ships with a codemod to automatically update your code:
npx @next/codemod@canary upgrade latest
Turbopack Is Now Stable for Dev
After years in beta, Turbopack (the Rust-based bundler) is stable for next dev in Next.js 15. It's not yet stable for production builds, but for development it's now the recommended default:
# next dev now uses Turbopack by default
next dev --turbopack
# Benchmarks vs Webpack on large apps:
# Cold start: ~53% faster
# Code updates: ~94% faster (HMR)
| Metric | Webpack | Turbopack | Improvement |
|---|---|---|---|
| Cold start (large app) | ~22s | ~10s | 53% faster |
| HMR (hot reload) | ~800ms | ~50ms | 94% faster |
| Memory usage | ~1.8GB | ~700MB | ~60% less |
3. Server Actions — The Right Way
Server Actions are now the canonical way to handle mutations in Next.js. They replace API routes for most form submissions and data writes.
Basic Pattern
// app/actions.ts — define server actions in a separate file
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
if (!title || !content) {
return { error: "Title and content are required" };
}
await db.post.create({ data: { title, content } });
revalidatePath("/blog"); // revalidate the blog listing page
return { success: true };
}
// app/blog/new/page.tsx — use the action directly in the form
import { createPost } from "@/app/actions";
export default function NewPostPage() {
return (
<form action={createPost} className="flex flex-col gap-4 max-w-lg">
<input
name="title"
placeholder="Post title"
className="border rounded px-3 py-2"
required
/>
<textarea
name="content"
placeholder="Write your post…"
className="border rounded px-3 py-2 h-40"
required
/>
<button
type="submit"
className="bg-[#0B9944] text-white rounded px-4 py-2 hover:bg-[#0a8a3d] transition-colors"
>
Publish
</button>
</form>
);
}
With useActionState for Error Handling
"use client";
import { useActionState } from "react";
import { createPost } from "@/app/actions";
export function NewPostForm() {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action} className="flex flex-col gap-4 max-w-lg">
<input name="title" placeholder="Post title" className="border rounded px-3 py-2" />
<textarea name="content" className="border rounded px-3 py-2 h-40" />
{state?.error && (
<p className="text-red-500 text-sm">{state.error}</p>
)}
{state?.success && (
<p className="text-emerald-600 text-sm">Post published!</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Publishing…" : "Publish"}
</button>
</form>
);
}
4. Partial Prerendering (PPR)
Partial Prerendering is Next.js 15's most ambitious feature. It allows a single route to be partially static and partially dynamic without splitting into multiple routes.
// app/product/[id]/page.tsx
import { Suspense } from "react";
import { ProductDetails } from "./product-details"; // static — prerendered
import { ProductReviews } from "./product-reviews"; // dynamic — streams in
import { RecommendedProducts } from "./recommended"; // dynamic — streams in
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* This is prerendered at build time — instant */}
<ProductDetails id={params.id} />
{/* These stream in dynamically — no blocking */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={params.id} />
</Suspense>
<Suspense fallback={<RecommendedSkeleton />}>
<RecommendedProducts id={params.id} />
</Suspense>
</div>
);
}
Enable it in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
ppr: true, // enable Partial Prerendering
},
};
PPR is still experimental in Next.js 15 but stable enough for production testing. It delivers the best of static and dynamic rendering in a single request with no extra configuration per route.
5. Upgrading from Next.js 14
Run the codemod first
npx @next/codemod@canary upgrade latest
This handles the most common breaking changes automatically:
- Async
paramsandsearchParams - Async
cookies()andheaders() useFormState→useActionStaterename
Breaking changes checklist
- •
fetch()no longer cached by default - •
paramsandsearchParamsare now Promises - •
cookies()/headers()are now async - •
useFormStaterenamed touseActionState - • Minimum Node.js version is now 18.18
- • React Compiler (auto memoisation)
- • Turbopack stable for dev
- • Partial Prerendering (experimental)
- •
use()hook for Promises and Context - • Stable
useOptimistic
Key Takeaways
Stop writing useMemo and useCallback. The React Compiler handles it at build time.
No more surprising stale data. Next.js 15 fetches fresh by default — you choose what to cache.
Replace most API routes with Server Actions. Less boilerplate, type-safe end-to-end.
Switch to Turbopack for dev. 94% faster HMR makes a real difference in large codebases.
Partial Prerendering lets one page be static and dynamic simultaneously. Rethink your page structure.
Don't migrate manually. The official codemod handles 80% of the breaking changes automatically.
Conclusion
Next.js 15 and React 19 are a genuinely significant release. The caching rethink alone removes one of the biggest sources of bugs and confusion in modern Next.js apps. The React Compiler means performance optimisation is increasingly something the toolchain handles, not the developer.
The shift toward Server Actions and async APIs feels like more boilerplate at first, but it produces code that is clearer about what runs where — which is exactly what large Next.js apps need.
The migration path is well-supported. Run the codemod, audit your fetch calls for caching intent, and upgrade your Node version. Most apps will be on Next.js 15 within a sprint.
Published by Ocean of Tech · March 10, 2026 · 11 min read
Tags: Next.js · React · Server Actions · React Compiler · Turbopack · App Router