DEV Community

Cover image for React 19 Features — What Actually Changed and What I Use
Safdar Ali
Safdar Ali

Posted on • Originally published at safdarali.in

React 19 Features — What Actually Changed and What I Use

React 19 shipped a laundry list of features. Twitter threads treated every hook like mandatory. In production on client sites and this portfolio, I adopted a subset — the ones that remove real bugs or UX jank — and ignored the rest until the ecosystem caught up. This is my honest react 19 features guide: what changed, code you can paste, and what I am still waiting on.

What actually changed at a high level

React 19 stabilised the Actions model (forms and mutations with pending state), added useOptimistic for instant UI feedback, introduced the use() hook for reading promises and context, improved hydration error messages, and made ref-as-prop cleaner. The compiler (React Forget) is separate — exciting, not required to upgrade.

Upgrade path: Next.js 15 projects already pin compatible React versions. Read RSC vs client components before mixing Actions with Server Components — boundaries still matter.

React 19 hooks and APIs — quick reference table

API Purpose Client / Server I use in prod?
useOptimistic Optimistic UI while mutation runs Client Yes
use() Read promise or context Both (with Suspense) Yes (with RSC)
useActionState Form action state Client Yes
useFormStatus Pending from parent form Client Yes
ref as prop No forwardRef boilerplate Both Gradual
Document metadata title, meta in components Client (limited) Prefer Next.js metadata

useOptimistic — instant feedback without lying to the user forever

Cart quantity updates, like buttons, todo toggles — users expect instant UI. useOptimistic shows the next state while the server catches up, then reconciles on success or rolls back on error.

"use client";
import { useOptimistic, useTransition } from "react";
import { updateQuantity } from "./actions";

type Item = { id: string; qty: number };

export function CartLine({ item }: { item: Item }) {
const [optimisticQty, setOptimisticQty] = useOptimistic(item.qty);
const [isPending, startTransition] = useTransition();

function changeQty(next: number) {
startTransition(async () => {
setOptimisticQty(next);
await updateQuantity(item.id, next);
});
}

return (
<div>
<button onClick={() => changeQty(optimisticQty + 1)} disabled={isPending}>
+
</button>
<span>{optimisticQty}</span>
</div>
);
}

// BEFORE — manual optimistic state with footguns
const [qty, setQty] = useState(item.qty);
const [pending, setPending] = useState(false);

async function bump() {
const prev = qty;
setQty(qty + 1); // optimistic
setPending(true);
try {
await updateQuantity(item.id, qty + 1);
} catch {
setQty(prev); // easy to forget rollback paths
} finally {
setPending(false);
}
}

// AFTER — useOptimistic + transition: rollback wired correctly

use() — promises and context without useEffect hacks

// Server Component passes a promise to client child
import { use } from "react";

type Product = { id: string; name: string };

function ProductList({ productsPromise }: { productsPromise: Promise<Product[]> }) {
const products = use(productsPromise); // suspends until resolved
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}

// Parent (Server Component) creates the promise once
export default function Page() {
const productsPromise = getProducts(); // do not await here
return (
<Suspense fallback={<p>Loading products…</p>}>
<ProductList productsPromise={productsPromise} />
</Suspense>
);
}

In production I use use() with Server Components streaming data — same mental model as async server fetch, less client useEffect spaghetti.

Actions and forms — less boilerplate than manual fetch

"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";

export function NewsletterForm() {
const [state, formAction, pending] = useActionState(subscribe, { ok: false, message: "" });

return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={pending}>{pending ? "Sending…" : "Subscribe"}</button>
{state.message && <p>{state.message}</p>}
</form>
);
}

Pair with Next.js Server Actions for mutations without a separate API route file — still validate on the server, still treat client state as untrusted.

What I immediately adopted

useOptimistic on any user-facing mutation where latency is felt on Indian mobile networks. useActionState / useFormStatus on marketing forms — fewer lines than custom pending flags. use() with Suspense boundaries on catalog sections fed from server promises. Better hydration errors — saved me an hour debugging a client-only chart imported into a Server Component (fixed by splitting the leaf).

// ref as prop — dropped forwardRef on new components
type ButtonProps = React.ComponentProps<"button"> & { ref?: React.Ref<HTMLButtonElement> };

export function Button({ ref, ...props }: ButtonProps) {
return <button ref={ref} {...props} />;
}

What I'm waiting on

React Compiler (Forget) — I will enable it per-route after stable Next.js integration docs, not on day one of React 19. Document metadata in client trees — I still use Next.js generateMetadata for SEO. Full ecosystem typings — some third-party libs lagged React 19 types for weeks; I pinned versions until they caught up.

I am also not rewriting every forwardRef component overnight — new code uses ref-as-prop; old code migrates on touch.

Waiting is a strategy, not laziness. The compiler will change how much manual memo we write — see my useCallback vs useMemo guide for why I am not adding more memo hooks while the ecosystem catches up.

My production setup

In production: React 19 + Next.js 15, Server Components by default, React 19 Actions on forms that need pending UX, optimistic updates on commerce interactions. Performance work still lives in caching and bundle size — see Next.js performance case study.

When experimenting, I use the workflow in Cursor + Claude for React — AI suggests React 19 APIs quickly, but I verify against official release notes before merge.

The single takeaway

React 19 is not a rewrite mandate. Adopt optimistic UI, Actions, and use() where they solve problems you already have. Wait on compiler and metadata experiments until your stack documents them.

Related: Next.js vs React learning path. Contact.

If this helped you

I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:

More guides on safdarali.in — same author, production-focused.

Top comments (0)