# Što ćete izgraditi#
Produkcijski React višekoračni obrazac ima zahtjeve s kojima se obrasci na jednoj stranici rijetko susreću: djelomično ispunjavanje, navigaciju naprijed-natrag, uvjetne korake, „skoro offline” ponašanje skica i analitiku kojoj možete vjerovati.
Ovaj vodič pokazuje pristup koji se može skalirati na duge wizardske tokove i složeno grananje, a pritom ostaje održiv. Koristit ćete React Hook Form za performantno upravljanje stanjem i Zod za validaciju, a zatim dodati perzistenciju skica na serveru i instrumentaciju.
Ako želite čvršću osnovu o arhitekturi obrazaca, krenite s React forms at scale with React Hook Form and Zod. Ako gradite na Next.js App Routeru, pogledajte i multi-step wizard with Server Actions and Zod te forms with React Hook Form, Zod, and Server Actions.
# Preduvjeti#
| Zahtjev | Verzija | Napomene |
|---|---|---|
| React | 18+ | Radi s bilo kojim routerom |
| React Hook Form | 7+ | Primjeri pretpostavljaju useForm i FormProvider |
| Zod | 3+ | Validacija „schema-first” |
| TypeScript | Preporučeno | Značajno povećava sigurnost kod grananja tokova |
| Backend perzistencija | Bilo koja | REST, GraphQL, Server Actions ili RPC |
ℹ️ Napomena: Ovaj članak koristi uzorke prilagođene Next.js-u, ali se osnovni dizajn primjenjuje na bilo koji React stack. Ključno je odvojiti UI koraka od perzistencije i pravila toka.
# Arhitektura: 4 problema koja morate riješiti#
React višekoračni obrazac koji preživi stvarni produkcijski promet treba četiri odvojena sloja. Njihovo miješanje je razlog zašto većina wizard tokova postane krhka.
| Sloj | Za što je zadužen | Što ne smije posjedovati |
|---|---|---|
| Stanje obrasca | Trenutne vrijednosti, dirana polja, greške na razini polja | Logiku routanja po koracima i server-side perzistenciju |
| Tok koraka | Koji koraci postoje, koji su vidljivi, redoslijed, pravila navigacije | Logiku validacije na razini polja |
| Validacija | Shema po koraku i konačna shema | UI komponente, server I/O |
| Perzistencija i analitika | Spremanje skice, nastavak, slanje, logiranje događaja | Poslovna pravila ugrađena u komponente |
Čest anti-pattern je stavljati odluke o toku u state komponenti poput setSteps([...]) i perzistirati cijelo React Hook Form stanje „kako jest”. To puca čim dodate uvjetne korake, verzioniranje shema ili podršku za nastavak starijih skica.
# Model podataka: dizajnirajte za skice i verzioniranje#
Perzistencija skica najbolje radi kada server sprema eksplicitan zapis skice, umjesto da sprema samo finalne predaje.
Praktičan model skice izgleda ovako.
| Polje | Tip | Svrha |
|---|---|---|
id | string | Stabilni identifikator dijeljen među uređajima |
userId | string nullable | Poveznica na autentificiranog korisnika kada je dostupno |
resumeToken | string nullable | Magic-link tok za anonimne korisnike |
status | enum | draft, submitted, abandoned |
schemaVersion | number | Omogućuje migracije kada se obrazac promijeni |
data | JSON | Djelomični odgovori, ne nužno valjani |
progress | JSON | Ključ trenutnog koraka i metapodaci o napretku |
updatedAt | timestamp | Sortiranje i čišćenje |
submittedAt | timestamp nullable | Revizija (auditing) |
Dva pravila sprječavaju bol kasnije:
- 1
datau skici mora prihvatiti djelomične vrijednosti, čak i nevaljane. - 2Finalna predaja mora proći validaciju prema strogoj shemi, čak i ako je spremanje skice toleriralo više.
🎯 Ključna poruka: Tretirajte „skicu” kao resurs prve klase s vlastitom shemom i životnim ciklusom, a ne kao nedovršenu predaju.
# Definicije koraka: neka koraci budu čisti i izvedivi#
Umjesto hardkodiranja liste koraka u JSX-u, definirajte korake kao podatke. Svaki korak treba deklarirati:
- stabilan
key title- polja koja posjeduje
- Zod podskup sheme za validaciju na razini koraka
isVisible(values)predikat za uvjetne korake- opcionalne navigacijske guardove
To drži uvjetnu logiku izvan stabala komponenti i čini analitiku konzistentnom.
```typescript
import \\{ z \\} from "zod";
export const fullSchema = z.object(\\{
accountType: z.enum(["personal", "business"]),
email: z.string().email(),
companyName: z.string().min(2).optional(),
vatId: z.string().min(6).optional(),
address: z.object(\\{
country: z.string().min(2),
city: z.string().min(2),
\\}),
\\});
export type FormValues = z.infer<typeof fullSchema>;
type StepDef = \\{
key: string;
title: string;
fields: (keyof FormValues | "address.country" | "address.city")[];
schema: z.ZodTypeAny;
isVisible?: (values: Partial<FormValues>) => boolean;
\\};
export const steps: StepDef[] = [
\\{
key: "account",
title: "Account",
fields: ["accountType", "email"],
schema: fullSchema.pick(\\{ accountType: true, email: true \\}),
\\},
\\{
key: "business",
title: "Business details",
fields: ["companyName", "vatId"],
schema: z.object(\\{
companyName: z.string().min(2),
vatId: z.string().min(6),
\\}),
isVisible: (v) => v.accountType === "business",
\\},
\\{
key: "address",
title: "Address",
fields: ["address.country", "address.city"],
schema: fullSchema.pick(\\{ address: true \\}),
\\},
];Korak business je uvjetan i vođen trenutnim vrijednostima. Nema mutiranja niza koraka, nema duplicirane logike grananja.
⚠️ Upozorenje: Izbjegavajte „samo uvjetno renderiranje” gdje sakrijete polja, ali ih ostavite registriranima s obveznom validacijom. To stvara skrivene greške koje blokiraju slanje i zbunjuje korisnike.
# React Hook Form postavke: jedan obrazac, više koraka#
Koristite jednu instancu useForm za cijeli wizard. Mijenjanje koraka ne smije ponovno stvarati obrazac, inače gubite praćenje „dirty” stanja, „touched” stanja i benefite performansi.
```tsx
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const draftSchema = fullSchema.partial();
export function WizardRoot(props: { initialData?: Partial<FormValues> }) {
const methods = useForm<FormValues>({
mode: "onBlur",
resolver: zodResolver(draftSchema),
defaultValues: props.initialData as any,
});
return (
<FormProvider {...methods}>
{/* Step router renders current step component */}
</FormProvider>
);
}Dva važna izbora ovdje:
- Koristite partial schema resolver za skice kako biste mogli spremati nedovršene vrijednosti bez trenutnih „hard” grešaka.
- I dalje pokrećite validaciju po koraku pri navigaciji te strogu validaciju pune sheme pri finalnom slanju.
# Strategija validacije: po koraku + finalna provjera#
Korisnici očekuju da im „Next” kaže što nedostaje na trenutnom koraku, a ne na budućem koraku koji još nisu vidjeli. Istovremeno, backend treba jednu strogu validaciju pri predaji.
Koristite ovaj obrazac:
- Na „Next”, validirajte samo polja iz sheme trenutnog koraka.
- Na „Submit”, validirajte punu shemu na serveru.
React Hook Form omogućuje okidanje validacije na specifičnim putanjama polja.
```tsx
import \\{ useFormContext \\} from "react-hook-form";
function useStepValidation(stepFields: string[]) \\{
const \\{ trigger \\} = useFormContext();
async function validateCurrentStep() \\{
return trigger(stepFields as any, \\{ shouldFocus: true \\});
\\}
return \\{ validateCurrentStep \\};
\\}Ako imate ovisnosti između polja unutar koraka, držite ih u toj shemi koraka. Ako imate ovisnosti između koraka, validirajte ih pri finalnom slanju i prikažite jasnu poruku uz link za skok natrag na relevantan korak.
# Uvjetni koraci: izračunajte vidljive korake iz vrijednosti#
Najjednostavnije skalabilno pravilo: lista koraka se uvijek treba izračunavati iz trenutnih odgovora, a ne spremati.
```ts
export function getVisibleSteps(
allSteps: typeof steps,
values: Partial<FormValues>
) {
return allSteps.filter((s) => (s.isVisible ? s.isVisible(values) : true));
}Kada korisnik promijeni accountType iz business u personal, business korak nestaje. Tada morate odlučiti što učiniti s njegovim podacima.
Postoje dvije sigurne politike:
| Politika | Što se događa s podacima skrivenog koraka | Kada koristiti |
|---|---|---|
| Zadrži podatke | Sprema se, ali ga finalna shema ignorira kroz refinement | Kad korisnici mogu često mijenjati izbor |
| Očisti podatke | Automatski resetira skrivena polja | Kad podaci utječu na usklađenost (compliance) ili cijene |
U B2B onboardingu, „zadrži podatke” je obično bolji UX. U reguliranim tokovima, „očisti podatke” sprječava slučajno slanje zastarjelih osjetljivih vrijednosti.
💡 Savjet: Ako čistite podatke, učinite to u trenutku kada uvjet postane false, ne pri renderu. Koristite
useEffectkoji promatra kontrolno polje i pozovitesetValuesashouldDirty: truekako bi auto-save to pokupio.
# Navigacija po koracima koja se nikad ne lomi#
Navigacija treba biti bazirana na ključevima koraka, ne na indeksima. Navigacija po indeksu puca čim se promijene uvjetni koraci.
Robustan model toka:
- ključ trenutnog koraka
- izvedeni ključevi vidljivih koraka
- pomoćne funkcije za next, previous ili skok na ključ ako je dopušteno
```ts
export function getNextStepKey(
visibleKeys: string[],
currentKey: string
) \\{
const idx = visibleKeys.indexOf(currentKey);
if (idx === -1) return visibleKeys[0];
return visibleKeys[idx + 1] ?? currentKey;
\\}
export function getPrevStepKey(
visibleKeys: string[],
currentKey: string
) \\{
const idx = visibleKeys.indexOf(currentKey);
if (idx <= 0) return currentKey;
return visibleKeys[idx - 1];
\\}Za „jump navigation” poput steppera, nemojte dopustiti preskakanje obveznih koraka bez validacije. Dopustite slobodan skok unatrag, a skok unaprijed samo na korake koji su već dovršeni.
Praćenje dovršenosti po koraku radite na temelju uspješne validacije, a ne na temelju samog postojanja vrijednosti. Dovršenost temeljena samo na prisutnosti je razlog zašto korisnici dobiju „complete” stanje s nevaljanim emailom.
# Spremanje skice: klijentski UX i server ugovori#
Spremanje skice nije feature, nego sustav. Korisnici očekuju da bude pouzdano, brzo i bez konflikata između tabova i uređaja.
Kada spremati#
Koristite tri okidača:
- 1Debounce spremanje nakon promjena.
- 2Spremanje pri prijelazu između koraka.
- 3Spremanje pri promjeni vidljivosti stranice (page visibility), ako je podržano.
Za debounce spremanje nemojte slati cijeli obrazac na svaki pritisak tipke kod velikih obrazaca. Dobra osnova je debounce između 800 i 1500 ms i slanje samo delti iz dirtyFields kad je moguće.
Što spremati#
Spremite:
datakao djelomične vrijednostiprogress.currentStepKeyprogress.visibleStepKeysako je korisno za debug, ali tretirajte to kao isključivo informativnoschemaVersion
Izbjegavajte spremati:
- UI flagove specifične za klijent
- validacijske greške
- touched stanje
Praktičan payload za skicu#
| Polje | Primjer | Napomene |
|---|---|---|
draftId | dr_123 | Stabilna referenca |
schemaVersion | 3 | Pokreće migracije |
data | partial JSON | Samo odgovori koje je korisnik unio |
progress | { currentStepKey: "address" } | Drži nastavak konzistentnim |
clientUpdatedAt | ISO string | Pomaže u rješavanju konflikata |
# Implementacija auto-savea s React Hook Form#
Koristite watch za praćenje promjena i debounce za smanjenje poziva. Uvijek se zaštitite od spremanja dok je prethodno spremanje još u tijeku.
```tsx
import { useEffect, useMemo, useRef, useState } from "react";
import { useFormContext } from "react-hook-form";
function useDebouncedDraftSave(saveFn: (data: any) => Promise<void>) {
const { watch, getValues, formState } = useFormContext();
const [saving, setSaving] = useState(false);
const lastSavedHash = useRef<string>("");
const subscription = watch(() => {
// no-op, subscription triggers effect
});
useEffect(() => {
const id = setTimeout(async () => {
if (!formState.isDirty || saving) return;
const values = getValues();
const hash = JSON.stringify(values);
if (hash === lastSavedHash.current) return;
setSaving(true);
try {
await saveFn(values);
lastSavedHash.current = hash;
} finally {
setSaving(false);
}
}, 1000);
return () => clearTimeout(id);
}, [subscription, formState.isDirty, saving, getValues, saveFn]);
return { saving };
}Ovaj jednostavan pristup šalje cijeli objekt. U većem opsegu radije spremajte patch. Ako je obrazac velik, JSON.stringify također može postati skup. Skalabilniji pristup je pratiti lagani hash po koraku ili slati samo patcheve iz dirtyFields.
⚠️ Upozorenje:
watch()se može okidati vrlo često. Ako vidite probleme s performansama, pretplatite se samo na polja trenutnog koraka ili izgradite step-level forme koje registriraju polja samo dok su mountana.
# Server-side perzistencija: endpointi, identiteti i konflikti#
Trebaju vam dvije server operacije:
createDraftkada wizard započneupdateDraftza naknadna spremanjasubmitDraftza finalizaciju i zaključavanje
Minimalni REST-style ugovor:
| Endpoint | Metoda | Svrha |
|---|---|---|
/api/drafts | POST | Kreira i vraća draftId |
/api/drafts/:id | PATCH | Sprema djelomične podatke i napredak |
/api/drafts/:id/submit | POST | Validira punu shemu i šalje |
Identitet: prijavljeni i anonimni#
Nastavak na drugom uređaju zahtijeva stabilan identitet:
- Za autentificirane korisnike, vežite skice uz
userId. - Za anonimne korisnike, izdajte
resumeTokeni pošaljite magic link e-mailom.
Spremite draftId na klijentu u localStorage kako biste nastavili na istom uređaju. Ne oslanjajte se samo na localStorage za cross-device.
Rješavanje konflikata#
Ako korisnik uređuje u dva taba ili uređaja, trebate politiku:
| Strategija | Kako radi | Kada koristiti |
|---|---|---|
| Last write wins | Server prepisuje skicu | Jednostavni consumer tokovi, nizak rizik |
| Provjera verzije | Odbije ako je clientUpdatedAt stariji od servera | Business-critical tokovi |
| Merge po polju | Server spaja patcheve | Složeno, samo ako je nužno |
U praksi, provjera verzije je dobar kompromis. Ako odbijete zbog konflikta, dohvatite najnoviju skicu sa servera, prikažite banner i pustite korisnika da nastavi s najnovijom.
# Next.js App Router: obrazac sa Server Actions#
Ako koristite App Router, Server Actions može pojednostaviti skice tako da validacija i perzistencija ostanu blizu servera. Bitno je i dalje isto: ažuriranja skice prihvaćaju djelomične podatke, a finalna predaja je stroga.
Cijeli primjer wizard toka po koracima možete vidjeti u Next.js App Router multi-step wizard with Server Actions and Zod.
Pojednostavljeni nacrt Server Actiona:
```ts
"use server";
import \\{ z \\} from "zod";
const draftUpdateSchema = z.object(\\{
draftId: z.string(),
schemaVersion: z.number(),
data: fullSchema.partial(),
progress: z.object(\\{
currentStepKey: z.string(),
\\}),
\\});
export async function saveDraftAction(input: unknown) \\{
const parsed = draftUpdateSchema.parse(input);
// Persist parsed.data and parsed.progress
// Return updatedAt and any migration hints
return \\{ ok: true \\};
\\}Za finalnu predaju, parsirajte strogo s fullSchema na serveru, ne na klijentu. To vas štiti od izmijenjenih zahtjeva i osigurava konzistentnu provedbu.
Ako želite više obrazaca oko Server Actions, referirajte se na Next.js forms with React Hook Form, Zod, and Server Actions.
# Nastavak kasnije: rehidracija i migracije sheme#
Nastavak nije samo postavljanje default vrijednosti. Morate pokriti slučajeve kada se obrazac promijenio otkad je skica kreirana.
Sigurna rehidracija#
Proces:
- 1Dohvatite podatke skice po
draftIdiliresumeToken. - 2Ako je
schemaVersionstariji, pokrenite migraciju na serveru. - 3Vratite normalizirane podatke koji odgovaraju trenutnom obliku sheme.
- 4Inicijalizirajte
defaultValuess tim normaliziranim podacima.
Nikad ne pokušavajte „best effort” migraciju na klijentu. To vodi do nekonzistentnog ponašanja između uređaja i čini debugging bolnim.
Primjer migracije sheme#
Migracija može biti jednostavno mapiranje, npr. razdvajanje jednog fullName u firstName i lastName, ili preimenovanje polja.
Držite migracije linearne i testabilne: v1 -> v2 -> v3. Izbjegavajte preskakanje izravno osim ako imate snažnu testnu pokrivenost.
# Instrumentacija analitike: učinite funnel korisnim#
Ako ne instrumentirate, ne možete poboljšati dovršenost. Višekoračni obrasci često imaju najveći drop-off koncentriran u jednom koraku, jednom polju ili na jednoj platformi.
Industrijski benchmarkovi variraju, ali dugi višekoračni tokovi često imaju 20 do 60 posto odustajanja ovisno o trenju, miksu uređaja i percipiranoj vrijednosti. Vaš cilj je brzo pronaći najveće točke trenja.
Pratite barem ove evente:
| Event | Kada | Svojstva koja trebate |
|---|---|---|
wizard_started | skica kreirana | draftId, schemaVersion, izvor ulaza |
step_viewed | korak mountan | stepKey, pozicija, uređaj |
step_completed | korak validiran i kliknut next | stepKey, vrijeme na koraku |
draft_saved | spremanje uspjelo | latencija, veličina payloada |
draft_resume | korisnik učitao postojeću skicu | dana od zadnje promjene |
wizard_submitted | uspjeh finalnog slanja | ukupno vrijeme, broj koraka |
wizard_error | server validacija pala | kodovi grešaka, mapiranje na korak |
Kako mapirati validacijske greške natrag na korake#
Server-side stroga validacija vratit će putanje polja. Upotrijebite definicije koraka kako biste mapirali svaku putanju greške na ključ koraka, pa vratite strukturirani error response kako bi UI mogao deep-linkati.
Primjer strategije mapiranja:
- svaki korak deklarira
fieldskoje posjeduje - putanja greške počinje rootom polja
- pronađite prvi korak gdje
fieldssadrži taj root
To vam omogućuje prikaz bannera tipa „Ispravite 2 problema u koraku Address” i gumb koji navigira tamo.
💡 Savjet: „Vrijeme na koraku” pratite kao
step_completed.timestamp - step_viewed.timestamp. Često je bolji signal trenja od broja grešaka, posebno na mobitelu.
# Sve zajedno: praktičan tok#
Skalabilna implementacija tipično ima ove dijelove:
- 1
steps.tss definicijama koraka i shemama. - 2
wizardFlow.tssgetVisibleSteps,getNextStepKey,getPrevStepKey. - 3
WizardRoot.tsxsFormProvider, učitavanjem skice i orkestracijom. - 4Komponente koraka koje samo renderiraju inpute, bez logike toka.
- 5Sloj perzistencije s
createDraft,saveDraft,submitDraft. - 6Analytics wrapper koji logira ključne evente.
Ključ je da su koraci „glupi”, tok je izveden, a server je izvor istine za skice i finalnu validaciju.
# Česte zamke u projektima React višekoračnih obrazaca#
- 1
Validiranje cijele sheme pri svakom kliku Next
Zatrpa korisnike greškama iz skrivenih koraka i povećava percipiranu složenost. - 2
Spremanje redoslijeda koraka u state i njegovo mutiranje
Uvjetni koraci razdesinkroniziraju navigaciju, status dovršenosti i analitiku. - 3
Spremanje skica bez verzioniranja sheme
Nakon jedne izmjene obrasca, stare skice se više ne mogu nastaviti i broj support ticketa eksplodira. - 4
Samo klijentska pohrana skice za „resume later”
Skice samo u localStorage ne rade među uređajima i lako se izgube nakon brisanja cachea. - 5
Nema server-side stroge validacije pri submitu
Klijentska validacija je korisna za UX, ali nije sigurnosna granica.
# Ključne poruke#
- Definirajte korake kao podatke sa stabilnim ključevima, predikatima vidljivosti i shemama po koraku kako biste izbjegli krhko uvjetno renderiranje.
- Koristite jednu instancu React Hook Forma za cijeli wizard, validirajte po koraku pri navigaciji i strogo validirajte na serveru pri submitu.
- Tretirajte skice kao server resurse prve klase sa
schemaVersion, djelomičnimdatai eksplicitnimprogressza nastavak među uređajima. - Navigaciju gradite po ključevima, ne po indeksima, i vidljive korake izračunavajte iz trenutnih vrijednosti pri svakom renderu.
- Instrumentirajte funnel: pregled koraka, dovršenje, latenciju spremanja, resume evente i mapiranje grešaka na korake kako biste gađali stvarne točke odustajanja.
# Zaključak#
React višekoračni obrazac koji sprema skice, nastavlja se među uređajima i podržava uvjetne korake je ponajprije arhitekturni problem, a ne UI problem. Kad odvojite definicije koraka, izvedbu toka, slojeve validacije i ugovore perzistencije, dobijete wizard koji može rasti bez lomljenja.
Ako želite da vam Samioda pomogne implementirati skalabilan wizard u Reactu ili Next.js-u, uključujući server-side skice, Zod validaciju i instrumentaciju analitike, kontaktirajte nas putem samioda.com i pregledat ćemo vaš trenutni tok te predložiti plan implementacije.
FAQ
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Web razvoj
Sve →UX obrasci za upload datoteka u Reactu: drag-and-drop, napredak, validacija i oporavak od grešaka
Praktičan vodič za UX u React uploadu datoteka: drag-and-drop, povratne informacije o napretku, validacija na klijentu, chunking, pauziranje i nastavak, optimistični UI te otporne strategije ponovnog pokušaja uz pristupačnu arhitekturu komponenti.
Upravljanje pristupom značajkama u Next.js (App Router): dozvole, prava korištenja i pristup prema naplati
Naučite obrasce za upravljanje pristupom značajkama u Next.js App Routeru: ograničite UI, server actions i API-je prema ulozi, planu, pravima korištenja i statusu Stripe pretplate uz dosljedne provjere.
Next.js + Stripe webhookovi u App Routeru: vodič za implementaciju s fokusom na pouzdanost
Vodič spreman za produkciju za Next.js Stripe webhookove u App Routeru — provjera potpisa, rad sa sirovim tijelom zahtjeva, idempotentnost, ponovni pokušaji, redoslijed događaja, Stripe CLI testiranje i rješavanje problema.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
React obrasci u velikim aplikacijama: React Hook Form + Zod obrasci za složene proizvode
Najbolje prakse za React obrasce u velikim aplikacijama uz React Hook Form i Zod: validacija po shemi, višekratno upotrebljiva polja, asinkrone provjere, višekoračni tokovi, performanse, pristupačnost i obrasci integracije sa serverom/API-jem.
Izrada višekoračnog čarobnjaka u Next.js App Routeru uz Server Actions + Zod (bez dodatnog API sloja)
Implementirajte produkcijski spreman Next.js višekoračni obrazac koristeći App Router Server Actions i Zod — s tri strategije stanja (kolačići, DB nacrti, URL), pristupačnim UX‑om, optimističnim prijelazima i robusnim rukovanjem greškama bez dodavanja API sloja.
Next.js App Router obrasci u 2026.: React Hook Form + Zod + Server Actions (validacija, greške, UX)
Arhitektura spremna za produkciju za Next.js App Router obrasce uz React Hook Form, Zod i Server Actions — sa zajedničkim shemama, sigurnom validacijom na serveru, async provjerama, uploadom datoteka i dosljednim rukovanjem greškama.