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:

  1. Upgrade React + react-dom to 19.
  2. Identify breaking changes (lots of small ones; codemods help).
  3. Adopt server actions and useOptimistic incrementally.
  4. 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

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 .