Tehnologija
useActionState deep dive: mutation flowovi, optimistični UI, Action queue i kad data layer i dalje pobjeđuje (React 19+)
Praktični deep dive u React 19 useActionState: kako radi Action queue, zašto su Transitions ključne za isPending, progressive enhancement s permalinkom, integracija useOptimistic za instant UX, otkazivanje putem AbortControllera i okvir odlučivanja — kada TanStack Query / SWR / RTK Query i dalje trebaju biti vlasnici cachea, retry logike i invalidacije.

React 2026 Primitives & Compiler Upgrade Guide
Ovo je poglavlje u React 2026 Primitives & Compiler Upgrade Guide. Fokus: useActionState — mutation flowovi, optimistic UX, semantika queuea i integracijski patterni.
Svi članci u ovom vodiču
01
Overview: React 2026 primitives and compiler-era mental model
Velika slika: arhitekturne promjene, što se promijenilo i gdje svaki primitiv ima smisla.
02
useActionState deep dive: mutation flowovi, optimistični UI i integracijski patterni
Praktični deep dive u Action queue, Transitions, optimistic UX i granicu gdje data layer i dalje pobjeđuje.
03
React <Activity />: zadrži state, pauziraj Effectse i renderaj u pozadini
Stvarni patterni, performance trade-offovi i pitfallovi za tabs/drawers/shell UI.
04
useEffectEvent deep dive: dizajn effecta, pretplate i analitika
Granice effecta koje su friendly prema linteru bez stale closurea ili reconnect churna.
Što dobivaš iz ovog deep divea
Ovo poglavlje je o korektnosti, UX-u i granicama ownershipa: što useActionState garantira, što ne — i kako ga koristiti bez reinvencije polupečenog data layera.
• Mentalni model za Actions + useActionState (koji state drži i gdje se posao izvršava). [1][2]
• Action queue: sekvencijalno izvođenje, zašto sprječava neke race bugove i kada postaje performance footgun. [2]
• Zašto
isPendingovisi o Transitions i dva sigurna načina kako triggerati actions. [2]• Progressive enhancement:
permalink, server response prije završetka hydrationa i što framework obično odradi umjesto tebe. [2][6]• Optimistic UX s
useOptimistic: instant UI, rollback i kako izbjeći stale optimistic state. [1][3]• Cancellation patterni s
AbortControllerkad user može “spammati” actions. [2]• Okvir odlučivanja: kad TanStack Query / SWR / RTK Query i dalje trebaju imati ownership nad cacheom, retryjima, invalidacijom i sinkronizacijom. [7][8][9]
• Sljedeće poglavlje: React
<Activity />deep dive (state retention + ponašanje Effectsa + rollout pitfallovi). [13]
Zašto useActionState postoji: učiniti mutation flowove dosadnima (na dobar način)
Većina aplikacija ima dvije glavne odgovornosti: prikazati podatke i promijeniti podatke. Loading je često jednostavan; kompleksnost je u mutacijama — korektnost, UX, debugiranje i state strojevi.
React 19 “Actions” smjer cilja standardizirati mutation handshake: pending state, optimistic UI i error handling bez ad-hoc state mašina za svaku formu ili gumb. [1]
useActionState je tanak, ali bitan sloj: omota Action, sprema zadnji rezultat kao state i izlaže isPending kako bi UI ostao responzivan tijekom async rada. I daje ključnu semantiku: dispatchi se stavljaju u queue i izvršavaju sekvencijalno. [2]
API semantika koja je bitna u produkciji
Signature je reducer-like: action prvo dobije previousState, zatim payload (za forme često FormData). Povratna vrijednost postaje sljedeći state. [2]
React može queueati više dispatcha i izvršavati ih sekvencijalno. Svako izvršavanje vidi prethodni rezultat kao previousState. To je feature korektnosti, ali znači i backpressure ako user triggera brže nego što se završava. [2]
Transitions nisu opcionalne. Ako dispatcher zoveš ručno, mora biti unutar Transitiona (startTransition). Ako ga proslijediš u action / formAction, React ga wrapa u Transition umjesto tebe. Inače isPending može biti pogrešan i React može upozoriti. [2]
Error handling je zamka: ako action baci exception, React može preskočiti sljedeće queued dispatchove. Za očekivane failove (validacija, 4xx) radije vrati error state nego throw. Throw ostavi za stvarno iznimne situacije za error boundary. [2]
Progressive enhancement je dio dizajna: permalink je za RSC-capable frameworke kako bi forma radila prije nego JS učita. Sa Server Functions, initial state mora biti serializable. [2]
Baseline pattern: submit forme s tipiziranim result stateom
Najbrži “win odmah” su forme: server-side validation errori, field errori i pending state koji pouzdano disablea submit bez prop drillinga. Reactovi primjeri to ciljaju kao first-class case. [1][2][4]
Praktičan pattern: modeliraj rezultat kao discriminated union da UI nikad ne “pogađa” što je u stateu.
import { useActionState } from "react";
type FieldErrors = { email?: string; password?: string };
type SubmitState =
| { kind: "idle" }
| { kind: "error"; message: string; fieldErrors?: FieldErrors }
| { kind: "success"; message: string };
async function register(
_prev: SubmitState,
formData: FormData,
): Promise<SubmitState> {
const email = String(formData.get("email") ?? "").trim();
const password = String(formData.get("password") ?? "");
const fieldErrors: FieldErrors = {};
if (!email.includes("@")) fieldErrors.email = "Enter a valid email.";
if (password.length < 8) fieldErrors.password = "Use at least 8 characters.";
if (fieldErrors.email || fieldErrors.password) {
return { kind: "error", message: "Fix the highlighted fields.", fieldErrors };
}
const res = await fetch("/api/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
return { kind: "error", message: "Registration failed. Try again." };
}
return { kind: "success", message: "Account created." };
}
export function RegisterForm() {
const [state, action, isPending] = useActionState<SubmitState, FormData>(
register,
{ kind: "idle" },
);
return (
<form action={action} className="space-y-3">
<div className="space-y-1">
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" />
{state.kind === "error" && state.fieldErrors?.email ? (
<p role="alert">{state.fieldErrors.email}</p>
) : null}
</div>
<div className="space-y-1">
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
{state.kind === "error" && state.fieldErrors?.password ? (
<p role="alert">{state.fieldErrors.password}</p>
) : null}
</div>
<button type="submit" disabled={isPending}>
{isPending ? "Creating…" : "Create account"}
</button>
{state.kind === "error" ? <p role="alert">{state.message}</p> : null}
{state.kind === "success" ? <p>{state.message}</p> : null}
</form>
);
}Jedan bitan detalj formi: React 19 form Actions mogu resetirati uncontrolled inpute nakon uspješnog submita, a React daje requestFormReset za ručne resete. Za “save draft” flowove obavezno testiraj uncontrolled vs controlled. [1]
Design system trik: useFormStatus uklanja prop-drilling pending statea
Ako imaš component library (Button, SubmitButton, FormFooter), provlačenje isPending kroz layer-e je naporno. useFormStatus čita parent form status kao context i dizajniran je baš za to. [4]
import { useFormStatus } from "react-dom";
type Props = { label: string };
export function SubmitButton({ label }: Props) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting…" : label}
</button>
);
}Progressive enhancement: action props, permalink i “radi prije nego JS učita”
React naglašava posebnu mogućnost kada se useActionState koristi sa Server Functions: framework može prikazati server response i održati formu interaktivnom ranije (progressive enhancement / prije završetka hydrationa). permalink postoji da pre-hydration submit zna kamo navigirati. [2]
U Next.js to su Server Functions (Server Actions u mutation kontekstu): async funkcije na serveru koje se pozivaju kroz mrežni request. Next ističe Transition wrapping i cache integraciju (jedan roundtrip može vratiti ažurirani UI i nove podatke). [6]
Korištenje <form action={dispatchAction}> automatski wrapa submit u Transition; React također spominje progressive enhancement za Server Functions i permalink. [2]
Next.js: Server Actions se integriraju u caching arhitekturu; <form action> / formAction se wrapaju u startTransition, a actions koriste POST. [6]
Optimistic UI bez samozavaravanja: useOptimistic + realna rollback priča
Optimistic UI nije “pretvaraj se da je brzo”. To je “prikaži očekivani final state dok je request pending i imaj čist oporavak na fail”. useOptimistic u React 19 je zamišljen kao par Actionsima: optimistic state postoji tijekom Actiona, a nakon završetka/faila React se vraća na source-of-truth. [1][3]
Ključni trik iz doksa: s useOptimistic(items, reducer) React može ponovno pokrenuti reducer ako se base prop items promijeni dok je action pending. To smanjuje stale optimistic state bugove u kolaborativnim ili skoro real-time UI-jevima. [3]
import { useActionState, useOptimistic } from "react";
type Todo = { id: string; title: string; pending?: true };
type AddState = { lastError: string | null };
type ApiResult =
| { ok: true; created: { id: string; title: string } }
| { ok: false; error: string };
async function addTodoOnServer(title: string): Promise<ApiResult> {
const res = await fetch("/api/todos", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title }),
});
if (!res.ok) return { ok: false, error: "Failed to create todo." };
const data: unknown = await res.json();
const created = data as { id?: unknown; title?: unknown };
if (typeof created.id !== "string" || typeof created.title !== "string") {
return { ok: false, error: "Bad response." };
}
return { ok: true, created: { id: created.id, title: created.title } };
}
function optimisticReducer(
state: Todo[],
action:
| { type: "add"; todo: Todo }
| { type: "replace"; tempId: string; real: Todo },
) {
if (action.type === "add") return [action.todo, ...state];
return state.map(t => (t.id === action.tempId ? action.real : t));
}
export function TodoComposer({ todos }: { todos: Todo[] }) {
const [optimisticTodos, applyOptimistic] = useOptimistic(todos, optimisticReducer);
const [state, submit, isPending] = useActionState<AddState, FormData>(
async (prev, formData) => {
const title = String(formData.get("title") ?? "").trim();
if (!title) return { lastError: "Title is required." };
const tempId = `temp:${Date.now()}`;
applyOptimistic({ type: "add", todo: { id: tempId, title, pending: true } });
const result = await addTodoOnServer(title);
if (!result.ok) return { lastError: result.error };
applyOptimistic({
type: "replace",
tempId,
real: { id: result.created.id, title: result.created.title },
});
return { lastError: null };
},
{ lastError: null },
);
return (
<div className="space-y-3">
<form action={submit} className="flex gap-2">
<input name="title" placeholder="Add a todo…" />
<button type="submit" disabled={isPending}>Add</button>
</form>
{state.lastError ? <p role="alert">{state.lastError}</p> : null}
<ul className="space-y-1">
{optimisticTodos.map(t => (
<li key={t.id}>
{t.title}{t.pending ? " (saving…)" : ""}
</li>
))}
</ul>
</div>
);
}Queued actions: kad sekvencijalna konzistentnost postane backpressure
Action queue je često podcijenjen detalj. Sekvencijalni dispatch uklanja klasu race bugova (kasni response ne pregazi noviji intent). Ali može stvoriti backlog: click-spam serijalizira mrežne pozive i UI djeluje tromo. React eksplicitno preporučuje useOptimistic, cancellation ili drugačiji pristup za kompleksne slučajeve. [2][3]
Cancellation je koristan za “latest intent wins”, ali nije besplatan: abort mrežnog requesta ne vraća server mutaciju unatrag. Najsigurnije je kad je side effect idempotentan, sigurno ignorabilan ili retryable. [2]
import { startTransition, useActionState, useRef } from "react";
type State = { value: number; error: string | null };
type Payload = { delta: number };
async function postDelta(delta: number, signal: AbortSignal): Promise<number> {
const res = await fetch("/api/counter", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ delta }),
signal,
});
if (!res.ok) throw new Error("Request failed");
const data: unknown = await res.json();
const next = (data as { value?: unknown }).value;
if (typeof next !== "number") throw new Error("Bad response");
return next;
}
export function Stepper() {
const abortRef = useRef<AbortController | null>(null);
const [state, dispatch, isPending] = useActionState<State, Payload>(
async (prev, payload) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
const nextValue = await postDelta(payload.delta, controller.signal);
return { value: nextValue, error: null };
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
return { value: prev.value, error: message };
}
},
{ value: 0, error: null },
);
function bump(delta: number) {
startTransition(() => dispatch({ delta }));
}
return (
<div className="space-y-2">
<div>Value: {state.value}{isPending ? " (updating…)" : ""}</div>
<div className="flex gap-2">
<button onClick={() => bump(-1)}>-</button>
<button onClick={() => bump(1)}>+</button>
</div>
{state.error ? <p role="alert">{state.error}</p> : null}
</div>
);
}React docs: AbortController može otkazati pending Actions, ali abort requesta ne poništava server-side mutacije. [2]
Snimka zaslona sekcije queued-actions-and-cancellationKad useActionState smanjuje boilerplate — i kad data layer i dalje posjeduje problem
Čista granica: useActionState je odličan za UI-owned mutation handshake; data layeri su odlični za sistemsku korektnost podataka (cache, retry, invalidacija, sinkronizacija između ekrana).
React 19 daje bolji primitiv za mutacije, ali namjerno ne pokušava zamijeniti odgovornosti data libraryja. I dalje moraš odlučiti tko posjeduje caching i sinkronizaciju. [1][2]
Jednostavna tablica odluke (heuristika, ne dogma):
| Potreba | useActionState | Data layer |
|---|---|---|
| Pending + field/server errori u formi | ✅ | ✅ |
| Jednostavna one-off mutacija s lokalnim UI updateom | ✅ | ✅ |
| Caching + dedupe + background refetch | ❌ | ✅ (SWR / TanStack Query) |
| Retry / backoff politike | ❌ | ✅ |
| Cache invalidacija preko više ekrana | ❌ | ✅ |
| Offline queue / persistence | ❌ | ✅ |
| Optimistic UI u malom scopeu | ✅ (s useOptimistic) | ✅ |
| Optimistic UI + shared cache reconciliation | ⚠️ | ✅ |Ako mutacija utječe na više queryja ili ekrana, obično želiš da biblioteka koja modelira taj graf posjeduje invalidaciju i refetch. TanStack Query tretira invalidaciju kao first-class workflow; RTK Query radi isto kroz tagove; SWR ima mutation + revalidation primitive iz sličnih razloga. [7][9][8]
Gdje useActionState briljira
Gdje data layer pobjeđuje
Integracijski pattern: useActionState kao UI shell, TanStack Query kao cache engine
Praktični hibrid: useActionState za success/error na razini forme, a TanStack Query za invalidaciju kako bi drugi ekrani reflektirali promjenu. TanStack docs pokazuju invalidateQueries kao standardni korak i napominju da Promise u onSuccess drži mutaciju pending dok se invalidacija ne završi. [7]
import { useActionState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
type State = { ok: boolean; message: string };
type Payload = { title: string };
declare function addTodo(input: Payload): Promise<void>;
export function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addTodo,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const [state, dispatch, isPending] = useActionState<State, FormData>(
async (_prev, formData) => {
const title = String(formData.get("title") ?? "").trim();
if (!title) return { ok: false, message: "Title is required." };
try {
await mutation.mutateAsync({ title });
return { ok: true, message: "Saved." };
} catch {
return { ok: false, message: "Failed to save." };
}
},
{ ok: true, message: "" },
);
return (
<form action={dispatch} className="flex gap-2">
<input name="title" placeholder="New todo" />
<button type="submit" disabled={isPending}>
{isPending ? "Saving…" : "Save"}
</button>
{state.message ? <span>{state.message}</span> : null}
</form>
);
}TanStack Query: invalidacija nakon mutacija kroz onSuccess + invalidateQueries (Promise.all za više ključeva). [7]
Snimka zaslona sekcije integration-example-react-queryBilješke o performansama i korektnosti (stvari koje kasnije grizu)
Većina regresija dolazi iz miješanja odgovornosti ili ignoriranja semantike: transitions, queueing, resetovi i veličina statea.
Praktična heuristika: u action state spremaj samo ono što UI treba nakon mutacije (errori, success, id-evi). Dugotrajni ili shared podaci trebaju biti na serveru, u framework cacheu ili u data layeru. [1][6][7]
Transition disciplina
Ručni dispatch mora biti unutar startTransition, inače React upozori i isPending neće raditi kako treba. action/formAction wrapa umjesto tebe. [2]
Queue backpressure
Sekvencijalni dispatch sprječava neke race bugove, ali može serijalizirati interakcije u backlog. Za rapid-fire: useOptimistic, cancellation ili drugačiji model. [2][3]
Veličina payload-a u stateu
Action state tretiraj kao UI state (errori, poruke, id-evi), ne kao cache. Veliki objekti povećavaju cijenu rendera i otežavaju reconciliation.
Reset ponašanje formi
React 19 form Actions mogu resetirati uncontrolled inpute nakon uspjeha; requestFormReset postoji za ručne resete. Draft/progress flowovi trebaju svjesnu strategiju. [1]
Rollout checklist koji možeš dati timu
Ako usvajaš useActionState kroz cijelu codebase, ove provjere sprječavaju najčešće UX i korektnost pitfallove.
• Modeliraj action result state kao discriminated union (izbjegni nejasne stateove).
• Za očekivane failove vrati error state umjesto throwa kako ne bi preskakao queued actions. [2]
• Ručni dispatch mora biti u
startTransition, inače jeisPendingvarljiv. [2]• Za “spammy” interakcije odluči: sekvencijalna konzistentnost, cancellation (AbortController) ili optimistic reducer. [2][3]
• Ako mutacija utječe na više ekrana, delegiraj invalidaciju/refetch data layeru (ili Next cache modelu). [7][9][6]
• Za Server Functions: osiguraj da je initial state serializable i testiraj progressive enhancement (
permalink/ pre-hydration submit). [2][6]• Testiraj reset ponašanje uncontrolled inputa i odluči trebaš li controlled inputs ili eksplicitni reset. [1]
ČPP
Ne. useActionState je odličan za UI handshake (pending + result + errors). Data layeri i dalje posjeduju caching, retry, invalidaciju, background refetch i sinkronizaciju između ekrana. Ako mutacija utječe na više queryja/routes, data layer (ili framework cache model poput Next.js) je obično pravi owner. [7][8][9][6]
Ako dispatcher zoveš ručno, mora biti unutar Transitiona (`startTransition`). Alternativno, proslijedi ga kao `action`/`formAction` — React će ga wrapati u Transition umjesto tebe. Inače React može upozoriti i `isPending` može biti netočan. [2]
Da: React queuea dispatchove i izvršava ih sekvencijalno, pa svako izvršavanje vidi prethodni rezultat kao `previousState`. To sprječava dio out-of-order bugova, ali može stvoriti backpressure u “spammy” flowovima. [2]
Koristi `useOptimistic` za instant UI, otkazi pending rad s `AbortController` kad želiš “latest intent wins”, ili razmisli o drugom modelu ako useActionState nije dobar fit. React docs eksplicitno navode te escape hatchove. [2][3]
Za očekivane failove (validacija, 4xx) vrati error state umjesto throwa. Ako action baci, React može preskočiti sljedeće queued dispatchove. Throw ostavi za stvarno iznimne slučajeve za error boundaries. [2]
`permalink` je za RSC-capable frameworke s progressive enhancementom. Ako se forma submit-a prije nego JS učita, browser može navigirati na permalink route umjesto da ovisi o trenutnom URL-u. Framework često odradi to umjesto tebe, ali je važno ako želiš “radi prije JS-a”. [2][6]
React 19 form Actions mogu automatski resetirati uncontrolled inpute nakon uspjeha. React ima i `requestFormReset` za ručne resete. Za draft/save-progress flowove namjerno odaberi uncontrolled vs controlled i testiraj ponašanje. [1]
Izvori
Uključujemo samo izvore koji direktno podupiru semantiku, upozorenja i integracijske tvrdnje korištene u ovom poglavlju.
• 1. React v19 (official): Actions, useActionState, useOptimistic, useFormStatus, form reset + requestFormReset Pročitaj izvor ↗
• 2. useActionState (official reference): queue semantics, Transitions requirement, permalink, cancellation, error handling caveats Pročitaj izvor ↗
• 3. useOptimistic (official reference): optimistic state semantics, reducer pattern, re-running reducer on new base data Pročitaj izvor ↗
• 4. useFormStatus (official reference): reading parent form status without prop drilling Pročitaj izvor ↗
• 6. Next.js (official): Updating Data with Server Functions (Server Actions), automatic Transition for form action props, caching integration, POST behavior Pročitaj izvor ↗
• 7. TanStack Query (official): Invalidations from Mutations (invalidateQueries + onSuccess promise semantics) Pročitaj izvor ↗
• 8. SWR (official): Mutation & Revalidation (useSWRMutation + cache update behavior) Pročitaj izvor ↗
• 9. Redux Toolkit (official): RTK Query Automated Re-fetching (cache tags invalidation model) Pročitaj izvor ↗
• 13. PAS7 Studio (published chapter): React <Activity /> deep dive (series next chapter reference) Pročitaj izvor ↗
Želiš uvesti Actions bez regresija?
useActionState je moćan kad su granice čiste: UI posjeduje mutation handshake, a sustav posjeduje korektnost shared podataka.
Ako radiš upgrade na React 19 (ili uvodiš Server Actions, optimistic UX i cache strategiju), PAS7 Studio može auditirati tvoje mutation flowove, postaviti granice ownershipa i isporučiti inkrementalni rollout plan s guardrailsima.
useActionState deep dive: mutation flowovi, optimistični UI i integracijski patterni