105: React 19 and 19.2 — Actions, Optimistic UI, Form Status, and Activity
Learning objectives
You will learn to:
- understand React Actions as async transitions;
- submit forms through function actions;
- manage Action state with
useActionState; - read parent form status with
useFormStatus; - model temporary optimistic state with
useOptimistic; - use
<Activity>to preserve hidden UI state while changing priority/visibility; - distinguish React Actions from API authorization and server-state caching;
- know when these APIs complement TanStack Query rather than replace it.
Actions mental model
An Action is async work executed inside a transition.
React can coordinate:
- pending state;
- optimistic UI;
- form submission;
- error propagation;
- transition scheduling.
A simple client Action:
async function saveTask(formData) {
const title =
String(
formData.get('title') ?? '',
).trim();
if (title.length < 3) {
return;
}
await createTask({ title });
}
function TaskForm() {
return (
<form action={saveTask}>
<input
name="title"
required
minLength={3}
/>
<button>Save</button>
</form>
);
}
Passing a function to <form action> is different from a traditional URL string action.
useActionState
useActionState helps when the Action should return state such as validation errors or a saved result.
import {
useActionState,
} from 'react';
async function createTaskAction(
previousState,
formData,
) {
const title =
String(
formData.get('title') ?? '',
).trim();
if (title.length < 3) {
return {
status: 'invalid',
message:
'Enter at least 3 characters.',
};
}
try {
const task =
await createTask({ title });
return {
status: 'success',
task,
message: '',
};
} catch {
return {
status: 'error',
message:
'Could not create task.',
};
}
}
function TaskForm() {
const [
state,
formAction,
isPending,
] = useActionState(
createTaskAction,
{
status: 'idle',
task: null,
message: '',
},
);
return (
<form action={formAction}>
<label htmlFor="title">
Title
</label>
<input
id="title"
name="title"
/>
<button disabled={isPending}>
{isPending
? 'Saving…'
: 'Save task'}
</button>
{state.message && (
<p
role={
state.status === 'error' ||
state.status === 'invalid'
? 'alert'
: 'status'
}
>
{state.message}
</p>
)}
</form>
);
}
useActionState is not server validation by itself. The server still validates and authorizes.
useFormStatus
A deeply nested submit button can read the nearest parent form status.
import {
useFormStatus,
} from 'react-dom';
function SubmitButton() {
const {
pending,
data,
method,
action,
} = useFormStatus();
return (
<button disabled={pending}>
{pending
? 'Saving…'
: 'Save'}
</button>
);
}
This avoids passing isPending through several component layers.
useFormStatus must be called by a component rendered inside the form whose status you want.
useOptimistic
Optimistic UI temporarily shows the expected result before the authoritative operation completes.
import {
useOptimistic,
} from 'react';
function TaskList({
tasks,
addTaskAction,
}) {
const [
optimisticTasks,
addOptimisticTask,
] = useOptimistic(
tasks,
(current, task) => [
...current,
{
...task,
optimistic: true,
},
],
);
async function action(formData) {
const title =
String(
formData.get('title') ?? '',
).trim();
const optimisticTask = {
id: `temp-${Date.now()}`,
title,
completed: false,
};
addOptimisticTask(
optimisticTask,
);
await addTaskAction({
title,
});
}
return (
<>
<form action={action}>
<input name="title" />
<button>Add</button>
</form>
<ul>
{optimisticTasks.map(
(task) => (
<li key={task.id}>
{task.title}
{task.optimistic
? ' (saving…)'
: ''}
</li>
),
)}
</ul>
</>
);
}
Optimistic data is speculative. The final server result remains authoritative.
If the server assigns IDs or normalizes data, reconcile with the response.
React Action versus TanStack Query mutation
These are not mutually exclusive concepts.
React Actions coordinate UI transitions and form behavior.
TanStack Query v5 coordinates server-state cache:
- cache entries;
- invalidation;
- stale/fresh state;
- refetching;
- mutation state;
- optimistic cache updates;
- pagination.
A production app may use a form Action and then invalidate/update relevant TanStack Query cache entries.
Do not duplicate server data into unrelated local state just because an Action returned it.
Server Functions
In an RSC-aware framework, a function marked "use server" can be called from client code as a Server Function.
Important:
"use server" does not mark a component as a Server Component.
There is no "use server" directive for declaring Server Components. It declares Server Functions.
Server Components and Server Functions are covered more deeply in lesson 115.
<Activity> in React 19.2
<Activity> lets React hide or show a subtree while preserving its state and managing its work priority.
A simplified conceptual example:
import {
Activity,
useState,
} from 'react';
function Dashboard() {
const [tab, setTab] =
useState('tasks');
return (
<>
<nav>
<button
onClick={() =>
setTab('tasks')
}
>
Tasks
</button>
<button
onClick={() =>
setTab('analytics')
}
>
Analytics
</button>
</nav>
<Activity
mode={
tab === 'tasks'
? 'visible'
: 'hidden'
}
>
<TaskWorkspace />
</Activity>
<Activity
mode={
tab === 'analytics'
? 'visible'
: 'hidden'
}
>
<Analytics />
</Activity>
</>
);
}
Use Activity when preserving hidden subtree state is valuable.
Do not use it automatically instead of conditional rendering. Hidden UI can still consume memory, and preserving state is not always desirable.
Pending UX
A good pending state:
- disables duplicate destructive submits when required;
- leaves useful context visible;
- uses
aria-busyor status text when appropriate; - does not lie about success before authority confirms it;
- supports retry on real failure.
Do not replace an entire form with a spinner if the user needs to understand what is being submitted.
Validation model
Use three levels:
- native/client constraints for immediate guidance;
- schema/client form validation where useful;
- server validation and authorization as authority.
An Action error such as:
Title is required
can be returned as form state.
An authorization failure such as:
You may not edit this task
must be enforced by the server.
Common mistakes
Calling optimistic setter outside an Action
useOptimistic is designed around Action/transition work.
Using Actions as a cache
Actions do not replace query freshness/invalidation.
Treating pending as success
Do not permanently show a server-generated task ID before the server returns it.
Confusing Server Components with "use server"
"use server" is for Server Functions.
Exercises
- Convert a controlled submit workflow to a form Action.
- Return validation errors through
useActionState. - Move submit pending UI into a child
SubmitButtonusinguseFormStatus. - Add an optimistic task and reconcile with the server result.
- Compare an optimistic Action with a TanStack Query v5 mutation design.
- Build tab panels with Activity and explain when preserving state is useful.
Exit questions
- What makes an Action different from an ordinary async function?
- What does
useActionStatereturn? - How does
useFormStatusfind the relevant form? - What is optimistic state?
- Why do Actions not replace authorization or a query cache?
- What problem does
<Activity>solve?
Official references
- https://react.dev/reference/react/useActionState
- https://react.dev/reference/react/useOptimistic
- https://react.dev/reference/react-dom/hooks/useFormStatus
- https://react.dev/reference/react/Activity
- https://react.dev/blog/2025/10/01/react-19-2
Deep dive: Actions unify async transition semantics, not server ownership
React Actions make certain async workflows easier to express, especially forms.
But an Action does not answer:
- where server data is cached;
- who authorizes the request;
- how data becomes stale;
- how pagination works;
- whether write is idempotent.
Those remain separate concerns.
Function form action lifecycle
function TaskForm() {
async function create(formData) {
const title = String(formData.get('title') ?? '').trim();
await saveTask({ title });
}
return (
<form action={create}>
<input name="title" />
<SubmitButton />
</form>
);
}
When the form action function succeeds, React can reset uncontrolled fields in appropriate form-action behavior.
If you control fields manually, you still own their reset state.
Test exact behavior for your React/framework version rather than assuming browser form behavior.
useFormStatus details
A child can read the nearest form submission:
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Creating…' : 'Create task'}
</button>
);
}
The component calling useFormStatus must be inside the form tree. The same component that renders the <form> cannot read that form's status before it exists as its parent; put status UI in a descendant.
useActionState previous state
Action signature:
async function action(previousState, formData) {
...
return nextState;
}
This is useful for accumulated form result state.
Example validation:
const initialState = {
values: {
title: '',
},
errors: {},
message: '',
};
async function createTaskAction(previousState, formData) {
const title = String(formData.get('title') ?? '').trim();
if (title.length < 3) {
return {
values: { title },
errors: {
title: 'Use at least 3 characters.',
},
message: 'Check the form.',
};
}
const result = await api.createTask({ title });
return {
values: { title: '' },
errors: {},
message: `Created ${result.task.title}`,
};
}
This result state is UI/form state, not a replacement for a query cache.
Action errors
Decide whether failure is:
expected validation → return structured state
or:
unexpected exception → throw / error boundary / monitoring as architecture dictates
Do not turn every server failure into an unhandled render exception.
Optimistic UI correctness
Optimism has three truths:
predicted UI request in progress authoritative server result
A robust workflow handles all transitions.
Create example:
const [optimisticTasks, addOptimisticTask] = useOptimistic(
tasks,
(current, optimisticTask) => [
...current,
optimisticTask,
],
);
The optimistic record may need:
{
clientId,
title,
status: 'sending'
}
When server returns:
{
id: 'server-123',
title: 'Normalized title'
}
reconcile IDs/data.
Do not let temporary client IDs leak permanently into URLs or relationships unless your API supports client-generated IDs.
Rollback versus error annotation
On failure, choices include:
- remove optimistic item;
- restore previous value;
- keep failed item with Retry;
- mark it failed and let user edit;
- show conflict resolver.
The correct choice depends on user value.
For a chat message, keeping failed message with Retry may be better than making it disappear.
For a destructive deletion, immediate rollback may be clearer.
Activity versus conditional rendering
Conditional:
{tab === 'editor' && <Editor />}
removes Editor when false, cleaning up state/effects.
Activity hidden mode can preserve internal state while hiding/deprioritizing subtree.
This trade-off affects:
- memory;
- preserved drafts;
- subscription lifecycle;
- privacy/sensitive fields;
- return speed.
Do not use Activity for a logout boundary where state must be destroyed.
Activity and Effects
When an Activity becomes hidden, React can clean up Effects while preserving state and restore them when visible.
This makes Activity more than CSS display: none.
Understand library/current documentation before assuming hidden activity continues every subscription.
Transition relationship
Actions operate within transition semantics, so React can coordinate pending rendering without blocking urgent inputs.
But long JavaScript work inside an Action still blocks the main thread. Scheduling cannot make CPU-heavy synchronous code free.
Move expensive computation or server work to appropriate systems.
Progressive enhancement and framework Actions
In RSC/server frameworks, form Actions can integrate with server functions and progressive enhancement.
Important separation:
React form Action = UI async transition mechanism
Server Function = privileged server boundary callable from client/framework
They often work together but are not identical concepts.
Server authorization example
Never:
async function deleteTaskAction(formData) {
const id = formData.get('id');
return db.task.delete(id);
}
without authorization.
Instead server function conceptually:
const user = await requireUser();
const task = await db.task.findById(id);
if (task.ownerId !== user.id) {
throw new ForbiddenError();
}
await db.task.delete(id);
The client-provided ID is untrusted.
Action versus TanStack Query mutation decision
Use React Action when:
- form/action semantics are central;
- framework server functions are used;
- pending/optimistic form state is enough.
Use TanStack Query mutation when:
- server cache needs invalidation/update;
- many consumers observe same resource;
- mutation lifecycle needs cache coordination;
- offline/network modes/pagination/infinite data matter.
Use together when responsibilities are clear.
Failure clinic
Optimistic success message before server truth
Do not show permanent "Saved" until authoritative success.
useFormStatus placed outside form
It will not observe the intended submission.
Server Function treated as trusted internal call
Client can invoke it with manipulated arguments. Authorize every operation.
Activity used to hide confidential state after logout
Preserving state may be wrong. Unmount/reset sensitive subtrees.
Exercises
- Return field errors from
useActionState. - Build a reusable submit button using
useFormStatus. - Implement optimistic create with temporary ID and server reconciliation.
- Compare rollback versus failed-item Retry UX.
- Replace conditional tab unmounting with Activity and observe state preservation.
- Write a server authorization checklist for one Action.
Mastery check
Explain:
- Action state versus server cache;
- optimistic prediction versus authority;
useFormStatusscope;useActionStateprevious state;- Activity preservation trade-offs;
- why server functions still require authorization.
Production case study: optimistic comment submission with retryable failed state
A disappearing failed optimistic item can be frustrating.
Instead, model optimistic comments:
{
clientId: 'local-1',
body: 'Looks good',
status: 'sending'
}
On failure:
{
clientId: 'local-1',
body: 'Looks good',
status: 'failed',
error: 'Network unavailable'
}
UI:
<li>
<p>{comment.body}</p>
{comment.status === 'sending' && (
<span>Sending…</span>
)}
{comment.status === 'failed' && (
<>
<span role="alert">Not sent.</span>
<button onClick={() => retry(comment.clientId)}>
Retry
</button>
</>
)}
</li>
For this product, "keep failed draft visible" is better than rollback disappearance.
For a bank transfer, optimistic success would be inappropriate.
The lesson: useOptimistic provides a mechanism; product risk determines whether optimism is appropriate and how failure is represented.
Additional depth: progressive enhancement and Action architecture
Function Actions become especially powerful in frameworks that can submit before all client JavaScript is ready.
A robust form should still have meaningful HTML:
<form action={createTaskAction}>
<label htmlFor="title">Title</label>
<input id="title" name="title" required minLength={3} />
<button>Create</button>
</form>
The browser platform remains the base.
React adds:
pending state Action state optimistic state transition coordination
Permalink concept
useActionState has an optional permalink mechanism for progressive-enhancement scenarios where the Action can be submitted before hydration and navigation needs a stable URL representation.
This is framework-oriented and should not be introduced unless the application uses server-rendered progressive forms, but learners should know why the API exists.
Action idempotency
A pending-disabled button reduces accidental duplicate clicks but cannot guarantee only one request.
Server-side operations such as:
charge payment create booking place order
may need idempotency keys or transactional uniqueness.
React pending state is UX, not distributed-systems protection.
Optimistic risk classification
Safe-ish optimism:
favorite toggle task checkbox local comment display
High-risk optimism:
payment succeeded inventory reserved legal approval recorded refund completed
For high-integrity operations, show pending then authoritative success rather than speculative success.
This is a product/risk decision, not a Hook preference.
