Web razvoj
ReactReact Hook FormZodNext.jsObrasciAnalitikaArhitektura

React višekoračni obrasci u velikom opsegu: spremanje skice, nastavak kasnije, validacija i navigacija po koracima

AO
Adrijan Omićević
·15 min čitanja

# Š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#

ZahtjevVerzijaNapomene
React18+Radi s bilo kojim routerom
React Hook Form7+Primjeri pretpostavljaju useForm i FormProvider
Zod3+Validacija „schema-first”
TypeScriptPreporučenoZnačajno povećava sigurnost kod grananja tokova
Backend perzistencijaBilo kojaREST, 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.

SlojZa što je zaduženŠto ne smije posjedovati
Stanje obrascaTrenutne vrijednosti, dirana polja, greške na razini poljaLogiku routanja po koracima i server-side perzistenciju
Tok korakaKoji koraci postoje, koji su vidljivi, redoslijed, pravila navigacijeLogiku validacije na razini polja
ValidacijaShema po koraku i konačna shemaUI komponente, server I/O
Perzistencija i analitikaSpremanje skice, nastavak, slanje, logiranje događajaPoslovna 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.

PoljeTipSvrha
idstringStabilni identifikator dijeljen među uređajima
userIdstring nullablePoveznica na autentificiranog korisnika kada je dostupno
resumeTokenstring nullableMagic-link tok za anonimne korisnike
statusenumdraft, submitted, abandoned
schemaVersionnumberOmogućuje migracije kada se obrazac promijeni
dataJSONDjelomični odgovori, ne nužno valjani
progressJSONKljuč trenutnog koraka i metapodaci o napretku
updatedAttimestampSortiranje i čišćenje
submittedAttimestamp nullableRevizija (auditing)

Dva pravila sprječavaju bol kasnije:

  1. 1
    data u skici mora prihvatiti djelomične vrijednosti, čak i nevaljane.
  2. 2
    Finalna 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
```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
```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
```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.

TypeScript
```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 korakaKada koristiti
Zadrži podatkeSprema se, ali ga finalna shema ignorira kroz refinementKad korisnici mogu često mijenjati izbor
Očisti podatkeAutomatski resetira skrivena poljaKad 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 useEffect koji promatra kontrolno polje i pozovite setValue sa shouldDirty: true kako bi auto-save to pokupio.

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
TypeScript
```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:

  1. 1
    Debounce spremanje nakon promjena.
  2. 2
    Spremanje pri prijelazu između koraka.
  3. 3
    Spremanje 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:

  • data kao djelomične vrijednosti
  • progress.currentStepKey
  • progress.visibleStepKeys ako je korisno za debug, ali tretirajte to kao isključivo informativno
  • schemaVersion

Izbjegavajte spremati:

  • UI flagove specifične za klijent
  • validacijske greške
  • touched stanje

Praktičan payload za skicu#

PoljePrimjerNapomene
draftIddr_123Stabilna referenca
schemaVersion3Pokreće migracije
datapartial JSONSamo odgovori koje je korisnik unio
progress{ currentStepKey: "address" }Drži nastavak konzistentnim
clientUpdatedAtISO stringPomaž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
```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:

  • createDraft kada wizard započne
  • updateDraft za naknadna spremanja
  • submitDraft za finalizaciju i zaključavanje

Minimalni REST-style ugovor:

EndpointMetodaSvrha
/api/draftsPOSTKreira i vraća draftId
/api/drafts/:idPATCHSprema djelomične podatke i napredak
/api/drafts/:id/submitPOSTValidira 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 resumeToken i 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:

StrategijaKako radiKada koristiti
Last write winsServer prepisuje skicuJednostavni consumer tokovi, nizak rizik
Provjera verzijeOdbije ako je clientUpdatedAt stariji od serveraBusiness-critical tokovi
Merge po poljuServer spaja patcheveSlož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:

TypeScript
```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:

  1. 1
    Dohvatite podatke skice po draftId ili resumeToken.
  2. 2
    Ako je schemaVersion stariji, pokrenite migraciju na serveru.
  3. 3
    Vratite normalizirane podatke koji odgovaraju trenutnom obliku sheme.
  4. 4
    Inicijalizirajte defaultValues s 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:

EventKadaSvojstva koja trebate
wizard_startedskica kreiranadraftId, schemaVersion, izvor ulaza
step_viewedkorak mountanstepKey, pozicija, uređaj
step_completedkorak validiran i kliknut nextstepKey, vrijeme na koraku
draft_savedspremanje uspjelolatencija, veličina payloada
draft_resumekorisnik učitao postojeću skicudana od zadnje promjene
wizard_submitteduspjeh finalnog slanjaukupno vrijeme, broj koraka
wizard_errorserver validacija palakodovi 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 fields koje posjeduje
  • putanja greške počinje rootom polja
  • pronađite prvi korak gdje fields sadrž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. 1
    steps.ts s definicijama koraka i shemama.
  2. 2
    wizardFlow.ts s getVisibleSteps, getNextStepKey, getPrevStepKey.
  3. 3
    WizardRoot.tsx s FormProvider, učitavanjem skice i orkestracijom.
  4. 4
    Komponente koraka koje samo renderiraju inpute, bez logike toka.
  5. 5
    Sloj perzistencije s createDraft, saveDraft, submitDraft.
  6. 6
    Analytics 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. 1

    Validiranje cijele sheme pri svakom kliku Next
    Zatrpa korisnike greškama iz skrivenih koraka i povećava percipiranu složenost.

  2. 2

    Spremanje redoslijeda koraka u state i njegovo mutiranje
    Uvjetni koraci razdesinkroniziraju navigaciju, status dovršenosti i analitiku.

  3. 3

    Spremanje skica bez verzioniranja sheme
    Nakon jedne izmjene obrasca, stare skice se više ne mogu nastaviti i broj support ticketa eksplodira.

  4. 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. 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čnim data i eksplicitnim progress za 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

Share
A
Adrijan OmićevićOsnivač i senior developer

Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.