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
Pregled: React 2026 primitives i mentalni model ere compilera
?ira arhitekturna slika: ?to se promijenilo i gdje svaki primitive stvarno pripada.
02
Dubinski vodi? za useActionState: mutation flows, optimistic UI i integration patterns
Kada useActionState smanjuje boilerplate, a kada data layer i dalje ostaje pravi vlasnik problema.
03
React <Activity />: zadr?i state, pauziraj Effects i renderiraj u pozadini
Stvarni obrasci, performance trade-offovi i pitfalls za tabs, drawers i shell UI.
04
Dubinski vodi? za useEffectEvent: dizajn Effecta, subscriptions i analitika
Linter-friendly granice Effecta bez stale closures i reconnect churna.
što dobivaš
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.
isPending ovisi o Transitions i dva sigurna načina kako triggerati actions. [2]AbortController kad user može “spammati” actions. [2]<Activity /> deep dive (state retention + ponašanje Effectsa + rollout pitfallovi). [13]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]
Ako tim zapamti samo jednu stvar, neka to bude ovaj flow. Većina konfuzije nestane kad je ownership eksplicitan.
User intent ulazi kroz Action
Submit forme ili ručni poziv dispatchera pokreće mutation flow. Ako dispatcher zoveš ručno, mora biti unutar startTransition. [2]
React prati pending i queuea sljedeće dispatchove
Svaki novi dispatch čeka prethodni, a svako izvršavanje dobije zadnji dovršeni rezultat kao previousState. [2]
Action vraća UI state, ne cache
Koristi vraćeni state za poruke, field error-e, id-eve i lokalni post-submit UX. Ne pretvaraj ga potajno u shared storage.
Sažetak
Tretiraj useActionState kao tanku UI ljusku oko mutacije, ne kao system of record.
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]
Ovo su greške zbog kojih useActionState djeluje lošije nego što jest. API je mali; misuse je najčešći problem.
Zvati dispatcher izvan startTransition i onda se čuditi zašto je isPending varljiv. [2]
Throwati validacijske/očekivane 4xx greške umjesto vratiti strukturirani error state. [2]
Trpati velike payloadove u action state i slučajno pretvoriti UI hook u pseudo-cache.
Sažetak
Većina boli u rollout-u dolazi iz miješanja odgovornosti, ne iz hooka.
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]
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>
);
}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 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>
);
}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-cancellationČ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
| Comparison point | useActionState paše | Data layer paše |
|---|---|---|
| Form pending + field/server errori | Da | Da |
| Jednostavna one-off mutacija s lokalnim UI updateom | Da | Da |
| Caching + dedupe + background refetch | Ne | Da (SWR / TanStack Query) |
| Retry / backoff politike | Ne | Da |
| Cache invalidacija preko više ekrana | Ne | Da |
| Offline queueovi / persistencija | Ne | Da |
| Optimistic UI za mali lokalni scope | Da (s useOptimistic) | Da |
| Optimistic UI + shared cache reconciliation | Rizično | Da |
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-queryVeć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]
ne pregovara se
Ručni dispatch mora biti unutar startTransition, inače React upozori i isPending neće raditi kako treba. action/formAction wrapa umjesto tebe. [2]
pazi spam pathove
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]
drži malo
Action state tretiraj kao UI state (errori, poruke, id-evi), ne kao cache. Veliki objekti povećavaju cijenu rendera i otežavaju reconciliation.
testiraj uncontrolled
React 19 form Actions mogu resetirati uncontrolled inpute nakon uspjeha; requestFormReset postoji za ručne resete. Draft/progress flowovi trebaju svjesnu strategiju. [1]
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]
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]
Uključujemo samo izvore koji direktno podupiru semantiku, upozorenja i integracijske tvrdnje korištene u ovom poglavlju.
• 4. useFormStatus (official reference): reading parent form status without prop drilling
• 8. SWR (official): Mutation & Revalidation (useSWRMutation + cache update behavior)
• 9. Redux Toolkit (official): RTK Query Automated Re-fetching (cache tags invalidation model)
• 13. PAS7 Studio (published chapter): React <Activity /> deep dive (series next chapter reference)
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.
Dubinski vodi? za useActionState: mutation flows, optimistic UI i integration patterns
Povezani članci
AI SEO / GEO u 2026: vaši sljedeći kupci nisu ljudi — nego agenti
Pretraživanje se pomiče s klikova na odgovore. Botovi i AI agenti pretražuju, citiraju, preporučuju i sve češće kupuju. Saznajte što znači AI SEO / GEO, zašto klasični SEO više nije dovoljan i kako PAS7 Studio pomaže brendovima pobijediti u agentičkom webu.
Najmoćniji Apple čip? M5 Pro i M5 Max ruše rekorde
Analiza Apple M5 Pro i M5 Max čipova u ožujku 2026. Objašnjavamo zašto se ovi čipovi mogu smatrati najjačim profesionalnim laptop SoC-ovima koje je Apple dosad napravio, kako izgledaju protiv M4 Pro, M4 Max, M1 Pro, M1 Max i što pokazuju u usporedbi s aktualnim Intel i AMD konkurentima.
Automatsko označavanje i pretraga spremljenih linkova
Integracija s GDrive/S3/Notion za automatsko označavanje i brzu pretragu putem search API-ja
Razvoj botova i usluge automatizacije
Profesionalni razvoj Telegram botova i automatizacija poslovnih procesa: chatbotovi, AI asistenti, CRM integracije, automatizacija radnih tijekova.
Profesionalni razvoj za vaše poslovanje
Kreiramo moderne web rješenja i botove za poduzeća. Saznajte kako vam možemo pomoći u postizanju ciljeva.