Exploring React 19: New Features and Improvements


React 19 is not a new framework. It is a set of defaults that match how many of us were already building with Next.js App Router: server-first data, forms that mutate, optimistic UI when the network lags.
I did not adopt everything on day one. This post covers what I use, when I use it, and what bit me. For where Client Components fit, read use client vs use server. For render cost, see React performance.
What: Wire form submission to an async function with pending state built in.
"use client";
import { useActionState } from "react";
async function updateName(prevState, formData) {
const name = formData.get("name");
try {
await updateUser(name);
return { success: true, message: "Name updated!" };
} catch {
return { success: false, message: "Failed to update name" };
}
}
function UpdateForm() {
const [state, formAction, isPending] = useActionState(updateName, null);
return (
<form action={formAction}>
<input name="name" placeholder="Enter your name" />
<button type="submit" disabled={isPending}>
{isPending ? "Updating..." : "Update Name"}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
When I'd use this: Mutations from Client Components — profile updates, settings, simple CRUD.
Gotcha: Actions are not a replacement for every fetch in useEffect. Server Actions in Next.js are the server-side sibling; know which runs where.
Split validation, mutation, and revalidation — one giant action with ten side effects becomes impossible to test.
use() hook — promises in renderWhat: Unwrap a promise (or context) inside render, paired with Suspense.
import { use, Suspense } from "react";
function UserProfile({ userPromise }) {
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
const userPromise = fetchUser();
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
When I'd use this: Client trees that receive a promise from a parent Server Component.
Gotcha: The promise should be created once per request/navigation — not inside render on every pass without caching, or you refetch infinitely.
When I skip it: Data I can fetch entirely on the server in a Server Component — simpler, less JS.
import { Suspense } from "react";
async function BlogPost({ params }) {
const post = await fetchPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<Suspense fallback={<CommentsSkeleton />}>
<Comments postId={post.id} />
</Suspense>
</article>
);
}
What: Async server components + Suspense boundaries for slow slices.
Why: Ship the article HTML while comments load.
Gotcha: Forgetting the boundary — one slow child blocks the whole page.
import { useOptimistic } from "react";
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function addTodo(formData) {
const newTodo = { text: formData.get("text") };
addOptimisticTodo(newTodo);
await saveTodo(newTodo);
}
return (
<div>
{optimisticTodos.map((todo) => (
<div key={todo.id}>
{todo.text}
{todo.pending && <span> (saving...)</span>}
</div>
))}
<form action={addTodo}>
<input name="text" />
<button type="submit">Add</button>
</form>
</div>
);
}
When I'd use this: Likes, todos, cart lines — user expects instant UI.
Gotcha: Rollback on failure. Optimistic UI without error handling trains users to distrust the app.
React 19 lets you drop <title> and <meta> next to the component that owns the page:
export default function BlogPost({ params }) {
return (
<>
<title>My Blog Post</title>
<meta name="description" content="Blog post description" />
<article>{/* content */}</article>
</>
);
}
In Next.js I still default to export const metadata or generateMetadata for static routes — less surprise with streaming and SEO tooling. Colocated metadata is great when your router model matches; do not fight the framework you already picked.
Pick one feature. Convert one form to Actions. Add one Suspense boundary. Measure bundle and UX before adopting the next.
React 19 rewards the same discipline as Next.js App Router: server by default, client where interactive, explicit loading and error states. The APIs just make that path shorter.