React 19 isn’t a paradigm shift. It’s a polish release — a cleaner mental model for forms, async data, and mutations. Once you’ve used it for a quarter, going back to 18 feels archaic. This is the practical writeup.
Server actions
async function createUser(formData: FormData) {
"use server";
const email = formData.get("email") as string;
await db.insert(users).values({ email });
revalidatePath("/users");
}
export default function NewUserForm() {
return (
<form action={createUser}>
<input name="email" />
<button>Create</button>
</form>
);
}
The function runs on the server. The form posts to it. Type-safe end to end. No API route. See Next.js 15 Server Components in Production .
use() hook
Read promises and contexts inline:
import { use } from "react";
function Posts({ promise }: { promise: Promise<Post[]> }) {
const posts = use(promise); // suspends until resolved
return posts.map(p => <li key={p.id}>{p.title}</li>);
}
Cleaner than the older async-as-state pattern. Works with Suspense for streaming.
useFormStatus
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}
The button knows its parent form’s pending state without prop drilling. Drop in any client component inside any form.
useOptimistic
"use client";
import { useOptimistic } from "react";
function Comments({ initial, action }: Props) {
const [optimistic, addOptimistic] = useOptimistic(
initial,
(state, newC: string) => [...state, { id: -1, text: newC, pending: true }]
);
return (
<>
<ul>{optimistic.map(c => <li>{c.text}{c.pending && " ..."}</li>)}</ul>
<form action={async (fd) => {
const text = fd.get("text") as string;
addOptimistic(text);
await action(fd);
}}>
<input name="text" />
<button>Send</button>
</form>
</>
);
}
Snappy UX with server-confirmed truth. Cleaner than rolling your own optimistic state.
useActionState
const [state, formAction] = useActionState(submit, null);
Holds form state + the action together. Supersedes useFormState from earlier.
Document metadata
function Post({ data }: { data: Post }) {
return (
<article>
<title>{data.title}</title>
<meta name="description" content={data.summary} />
...
</article>
);
}
Inline <title> / <meta> deduped automatically. No third-party react-helmet.
Asset preloading
import { preload, preinit } from "react-dom";
preload("/font.woff2", { as: "font" });
preinit("/critical.css", { as: "style" });
First-class API for the resource hints you used to write by hand.
What’s harder
- Mental model split between Server Components and Client Components is real.
"use client"boundaries — getting them right requires thought.- Caching defaults are aggressive — you’ll explicitly bust caches more often than you expect.
For the patterns: Next.js 15 Server Components in Production .
Migration
For an existing React 18 app:
- Upgrade React + react-dom to 19.
- Identify breaking changes (lots of small ones; codemods help).
- Adopt server actions and
useOptimisticincrementally. - Keep client-side state where it lives.
For new apps in 2026: start on React 19 + Next 15. Don’t look back.
Read this next
- Next.js 15 Server Components in Production
- SvelteKit vs Astro for Content Sites
- Modern TypeScript Backend with Hono on Bun
- Drizzle ORM Deep Dive
If you want a React 19 + Next 15 + Drizzle starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .