React 19 in Practice: Concurrent Features, Actions, and the New Hooks
Why React 19 Is Worth the Upgrade
React 19 formalizes years of experimental concurrent features into stable APIs and closes the long-standing gap around "forms / data mutations." The core idea: wrap writes in Actions and reads in concurrent primitives, so the UI never freezes while waiting.
| Capability | React 18 | React 19 |
|---|---|---|
| Server Components | experimental (via frameworks) | stable, document-level feature |
| Form submission | hand-rolled useState + onSubmit |
useActionState / <form action> |
| Reading Promise / Context | use unavailable |
use() reads Promise/Context in render |
| ref | only useRef + forwardRef |
passed directly as a prop |
| Optimistic update | hand-rolled state toggling | useOptimistic |
| Concurrent transition | useTransition |
useTransition + enhanced useDeferredValue |
Actions: First-Class Writes
An Action is a function React can "wait for." Combined with <form action> or useActionState, React manages the pending state and disables the submit button automatically.
"use client";
import { useActionState } from "react";
async function subscribe(prevState: { ok: boolean; msg: string }, formData: FormData) {
const email = formData.get("email");
const res = await fetch("/api/subscribe", {
method: "POST",
body: JSON.stringify({ email }),
});
if (!res.ok) return { ok: false, msg: "Subscription failed, please retry" };
return { ok: true, msg: "Subscribed!" };
}
export function SubscribeForm() {
const [state, formAction, pending] = useActionState(subscribe, {
ok: false,
msg: "",
});
return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={pending}>{pending ? "Submitting…" : "Subscribe"}</button>
<p>{state.msg}</p>
</form>
);
}
useFormStatus lets a child component read the parent form's status without prop drilling:
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}
Organize config data (e.g. form defaults returned by an API) with the JSON Formatter tool before feeding it into
initialStateto avoid hand-writing large objects.
use: Read Promises and Context in Render
use() is React 19's "read during render" primitive: it can directly await a Promise or read a Context, and it suspends at the nearest Suspense boundary.
import { use } from "react";
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise); // suspends until resolved
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.body}</li>
))}
</ul>
);
}
Caveat: use() must be called during the render phase of a component or Hook, not inside conditional branches outside try/catch — it is not an ordinary function but a signal to React that "this may need to wait."
Concurrent Rendering: Stay Responsive While Waiting
useTransition: mark "slow updates" as non-urgent
import { useTransition, useState } from "react";
function Search() {
const [query, setQuery] = useState("");
const [list, setList] = useState<Item[]>([]);
const [isPending, startTransition] = useTransition();
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // input responds instantly
startTransition(() => {
setList(filterExpensive(value)); // heavy compute wrapped in transition
});
}
return (
<div>
<input value={query} onChange={onChange} />
<ul style={{ opacity: isPending ? 0.6 : 1 }}>
{list.map((i) => (
<li key={i.id}>{i.name}</li>
))}
</ul>
</div>
);
}
useDeferredValue: automatically "defer" a value
When input and rendering are decoupled, useDeferredValue is simpler:
const deferred = useDeferredValue(query);
const results = useMemo(() => filterExpensive(deferred), [deferred]);
Difference: useTransition wraps "a span of state updates"; useDeferredValue derives "a deferred value," usually with useMemo.
useOptimistic: Optimistic Updates Out of the Box
import { useOptimistic, useRef } from "react";
type Msg = { id: number; text: string; sending?: boolean };
function Chat() {
const [messages, setMessages] = useOptimistic<Msg[]>([]);
async function send(formData: FormData) {
const text = formData.get("text") as string;
setMessages((list) => [...list, { id: Date.now(), text, sending: true }]);
await fetch("/api/chat", { method: "POST", body: JSON.stringify({ text }) });
}
return (
<form action={send}>
<input name="text" />
{messages.map((m) => (
<p key={m.id}>{m.text}{m.sending ? " (sending)" : ""}</p>
))}
</form>
);
}
For theme colors and button contrast, verify UI accessibility (WCAG) with the Color Converter and Color Contrast tools.
ref as a Prop and Document Metadata
ref is no longer special
function MyInput({ ref }: { ref: React.Ref<HTMLInputElement> }) {
return <input ref={ref} />;
}
// usage: <MyInput ref={myRef} /> — no forwardRef needed
Native document metadata
import { Title, Meta } from "react-dom";
function Page() {
return (
<>
<Title>Page Title</Title>
<Meta name="description" content="Page description" />
</>
);
}
Smooth Migration from React 18
- Upgrade deps:
npm i react@19 react-dom@19and upgrade@types/react. - Replace
forwardRef: just putrefin props; the old form is deprecated but still works for now. - Adopt Actions: migrate "submit then setState + loading" forms to
useActionState. - Use
usefor async: pass data Promises to children under Server Components and read withuse(). - Add concurrency: use
useDeferredValueoruseTransitionfor list/search interactions.
FAQ
Q1: Can Actions only be used in Server Components?
No. Client components can also wrap ordinary async functions with useActionState; a Server Action is just one form where the function runs on the server.
Q2: What's the difference between use() and await?
await is a JS runtime concept; use() is a React render concept: it suspends at a Suspense boundary and can be interrupted and re-rendered by concurrent scheduling.
Q3: Which to pick, useTransition or useDeferredValue?
Use useTransition to "defer a whole span of updates"; use useDeferredValue + useMemo to "derive a slow result from a fast-changing value."
Q4: Does React 19 still support IE?
No, and legacy behaviors have been removed. A modern browser environment is required.
Q5: How do I roll back a failed optimistic update?
useOptimistic automatically rolls back to the real state when the Action throws; just ensure the "real data source" behind setMessages is unchanged on failure.
Recommended Tools
In React 19 development, these tools help:
- JSON Formatter — organize API responses and form initial state
- Color Converter — HEX/RGB/HSL conversion for theming
- Color Contrast — verify button/text WCAG accessibility
- HTTP Status Codes — debug Action API response codes
The essence of React 19 is not "a few more Hooks," but a unified mental model that governs writes (Actions) and reads (concurrency/use): the UI stays responsive while waiting for data and self-manages state while submitting changes. Master Actions plus concurrent primitives, and you hold the key to modern React.
Try these browser-local tools — no sign-up required →