React 19 Actions & Async UI ⚡
React 19 (stable since Dec 5, 2024) streamlines async workflows with first-class action primitives, pending UI helpers, and optimistic rendering hooks.
1) Actions Concept 🧠
- An action is any async function passed to React APIs (forms, transitions) that returns data or throws errors.
- React knows when an action is running and exposes pending/error state automatically.
async function saveProfile(formData) {
"use server"; // optional in RSC contexts
const response = await fetch("/api/profile", {
method: "POST",
body: formData
});
return response.json();
}Actions can run client-side (regular async functions) or server-side (RSC with "use server").
2) Async Transitions + Pending UI 🔄
- Wrap slow state updates in
startTransitionoruseTransitionto keep urgent interactions snappy. - When transitions wrap actions, React marks them as non-blocking and exposes
isPending.
const [isPending, startTransition] = useTransition();
function handleSearch(term) {
startTransition(async () => {
const results = await searchAction(term);
setResults(results);
});
}Render fallback UI while isPending is true.
3) Form Actions 📄
React 19 lets you attach async functions directly to forms with <form action={fn}>.
function ProfileForm() {
return (
<form action={updateProfile}>
<input name="displayName" />
<SubmitButton />
</form>
);
}- React automatically gathers
FormData, invokesupdateProfile, handles pending state, and can reset the form on success. - No manual
onSubmit+preventDefaultboilerplate.
4) useActionState 🧾
Tracks the state returned by an action plus pending/errors.
const [state, action, isPending] = useActionState(updateProfile, { message: "" });
return (
<form action={action}>
<input name="displayName" />
<button disabled={isPending}>Save</button>
{state.message && <p>{state.message}</p>}
</form>
);statemirrors the latest resolved value from the action.- Great for surfacing success messages or server validation results.
5) useFormStatus 🧠
Read the pending/error state of the closest parent form inside nested components.
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Saving..." : "Save"}
</button>
);
}- Works even if the button is several levels deep in the component tree.
- Encourages reusable buttons, status badges, and loading indicators.
6) useOptimistic ✨
Show optimistic UI while awaiting action results.
import { useOptimistic } from "react";
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos);
async function addTodoAction(formData) {
const title = formData.get("title");
addOptimisticTodo(prev => [...prev, { id: crypto.randomUUID(), title, pending: true }]);
return createTodoOnServer(title);
}
return (
<form action={addTodoAction}>
<input name="title" />
<button>Add</button>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.pending ? "🟡" : "🟢"} {todo.title}</li>
))}
</ul>
</form>
);
}- Automatically reconciles optimistic state when the real result arrives (or reverts on error).
Key Takeaways ✅
- Actions reduce boilerplate: attach async logic directly to transitions and forms.
useActionState+useFormStatusexpose pending + result state without prop drilling.useOptimisticdelivers polished UX while awaiting confirmation.- Form actions make React feel like a full-stack framework when paired with RSC.
Recap 🔄
React 19 treats async work as a first-class citizen: define actions once, wire them to transitions or forms, track progress via useActionState/useFormStatus, and delight users with useOptimistic. Less glue code, more resilient UX.