Zpět na domů

Formuláře v Next.js jako kontrakt: Zod a jednotná pravidla

Článek vysvětluje, jak považovat formuláře v Next.js ne za UI-komponenty, ale za kontrakty mezi rozhraním, validací a serverem. Použití Zod, safeParse a flatten umožňuje sjednotit pravidla, formát chyb a stavy, zajišťuje předvídatelný UX a absence nesrovnalostí mezi klientem a serverem.

Jak proměnit formuláře v Next.js v spolehlivý kontrakt pomocí Zod
Advertisement 728x90

# Formuláře v Next.js jako kontrakt: Zod, fieldErrors a jednotná pravidla na klientovi a serveru

Formulář není jen sada polí a tlačítek, ale smlouva mezi rozhraním, validací a zdrojem dat. Když není tato smlouva explicitně definována, vzniká chaos: UI považuje data za správná, server vrací chybu, zprávy se zobrazují kdekoliv a chování formuláře je nepředvídatelné. Řešení – sjednotit schéma validace, formát chyb a stavy pomocí Zod, safeParse a flatten, čímž zajistíte stejná pravidla na klientovi i serveru.

Proč se formuláře rozpadají bez kontraktu

Mnoho projektů začíná jednoduše: input, state, submit. Dokud je formulář malý – vše funguje. Ale s růstem kódu se objeví tři kritické body nesouladu:

  • Různá pravidla validace. Klient kontroluje délku řetězce, server navíc zakázaná slova nebo mezery. Uživatel vidí zelený háček, ale uložení selže.
  • Nekonzistentní formát chyb. Jednou přijde řetězec, jindy objekt, potřetí pole. UI musí hádat strukturu odpovědi.
  • Neurčitá stavy. Pending, disable, success, formError – každý komponent je realizuje po svém, což vede k fragmentovanému UX.

Bez jednotné smlouvy žije každý formulář podle svých lokálních pravidel. To se nedaří škálovat.

Google AdInline article slot

Formulář jako kontrakt: klíčové prvky

Smlouva formuláře musí jasně odpovídat na tyto otázky:

  • Jaká data se považují za platná?
  • Které chyby se týkají konkrétních polí (fieldErrors), a které celé operace (formError)?
  • Jak se zpracovává stav pending?
  • Co se vrací při úspěchu?
  • Může server vrátit odpověď v jiném formátu?
  • Používají se stejná pravidla na klientovi i serveru?

Odpovědi na tyto otázky musí být stanoveny před zahájením implementace UI. V Workbench tento přístup umožnil standardizovat formuláře kolem schémat Zod a jednotného tvaru odpovědi.

Jedno schéma Zod – dvě použití

Místo duplikování logiky validace na klientovi a serveru použijte jedno schéma Zod, aplikované na obou místech. Příklad schématu pro poznámku:

Google AdInline article slot
import { z } from "zod";

export const noteSchema = z.object({
  title: z
    .string()
    .trim()
    .min(3, "Enter minimum 3 simvola")
    .max(80, "Maximum 80 characters"),
  description: z
    .string()
    .trim()
    .max(500, "Maximum 500 characters")
    .optional()
    .or(z.literal("")),
}).refine(
  data => data.title.toLowerCase() !== "test",
  {
    path: ["title"],
    message: "Withlishkom tekhnicheskoe name for zametki",
  }
);

export type NoteInput = z.infer<typeof noteSchema>;

Toto schéma definuje platné hodnoty, transformace (např. trim) a vlastní pravidla (refine). Stává se jediným zdrojem pravdy pro validaci.

Bezpečná validace přes safeParse

Použití safeParse místo parse je pro formuláře klíčové. parse vyhazuje výjimku při chybě – to je špatné pro UX, kde je neplatný vstup očekávaný. safeParse vrací řiditelný výsledek:

export function validateNote(input: unknown) {
  return noteSchema.safeParse(input);
}

Výsledek obsahuje příznak success a případně objekt error. To umožňuje UI i serverové logice pracovat se stejným typem odpovědi bez try/catch.

Google AdInline article slot

Sjednocení chyb přes flatten

Zod vrací složitý objekt chyby. Pro formuláře stačí dvě úrovně: chyby polí a obecná chyba formuláře. Metoda flatten() je ideální:

export function formatZodError(error: import("zod").ZodError) {
  const flat = error.flatten();

  return {
    fieldErrors: flat.fieldErrors,
    formError: flat.formErrors[0] ?? null,
  };
}

Příklad rozdílu:

  • fieldErrors: „Pole title nemůže být prázdné“, „Popis překračuje 500 znaků“.
  • formError: „Poznámka s tímto slugem již existuje“, „Server je dočasně nedostupný“.

Toto rozdělení činí UX předvídatelným: uživatel přesně ví, kde opravit vstup a kde opakovat akci nebo počkat.

Jednotná smlouva na klientovi a serveru

Na klientovi se schéma Zod používá pro časnou validaci a řízení stavu UI:

const clientResult = useMemo(() => {
  return noteSchema.pick({ title: true }).safeParse({ title });
}, [title]);

const titleError = clientResult.success
  ? null
  : clientResult.error.flatten().fieldErrors.title?.[0] ?? null;

const canSubmit = clientResult.success;

Na serveru slouží stejné schéma jako poslední linie obrany:

export async function createNoteAction(raw: unknown) {
  const parsed = noteSchema.safeParse(raw);

  if (!parsed.success) {
    const flat = parsed.error.flatten();
    return {
      ok: false,
      fieldErrors: flat.fieldErrors,
      formError: flat.formErrors[0] ?? null,
    };
  }

  try {
    // zápis do databáze
    return {
      ok: true,
      fieldErrors: {},
      formError: null,
    };
  } catch {
    return {
      ok: false,
      fieldErrors: {},
      formError: "Not succeeded sokhranit zametku",
    };
  }
}

Pravidlo je jedno – role různé: UX na klientovi, bezpečnost na serveru.

Standardní tvar stavu formuláře

Aby se zabránilo fragmentaci, definujte jednotný typ pro všechny formuláře:

export type FormState = {
  ok: boolean;
  fieldErrors: Record<string, string[] | undefined>;
  formError: string | null;
};

Tento tvar pokrývá všechny možné stavy: úspěšné provedení, chyby polí, obecné chyby. Všechny formuláře v projektu vracejí stejnou strukturu – to zjednodušuje zpracování na úrovni UI.

Kdy je potřeba React Hook Form

React Hook Form (RHF) je silný nástroj, ale ne univerzální řešení. Je oprávněný, pokud formulář obsahuje:

  • Mnoho polí se závislou validací
  • Dynamické seznamy nebo vnořené struktury
  • Validaci na blur, stavy touched/dirty
  • Vlastní kontroly s řízením fokusu
  • Potřebu optimalizace re-renderů

Pro jednoduché formuláře s 1–2 poli a základní validací přidává RHF zbytečnou složitost. Pokud derived state přes useMemo + Zod řeší úkol – nezobtěžujte.

Změna myšlení: od UI ke kontraktu

Hlavní posun – přestat vnímat formulář jako komponent UI. Formulář je smlouva, která definuje:

  • Schéma platných dat
  • Formát vracení chyb
  • Chování při různých stavech (pending, success, error)
  • Hranice odpovědnosti klienta a serveru

Když je smlouva definována předem, implementace UI se stává mechanickou úlohou. A nejdůležitější – každý formulář v projektu se chová předvídatelně, protože dodržuje stejnou dohodu.

Co je důležité

  • Používejte jedno schéma Zod pro klienta i server – to vylučuje nesoulady v pravidlech.
  • Vždy aplikujte safeParse – vrací řiditelný výsledek místo výjimek.
  • Rozdělujte fieldErrors a formError – to zlepšuje UX a zjednodušuje logiku zobrazení.
  • Definujte standardní tvar FormState – všechny formuláře v projektu musí vracet stejnou strukturu.
  • Připojujte React Hook Form pouze při skutečné potřebě – ne kvůli módě, ale pro řešení složitých scénářů.

— Editorial Team

Advertisement 728x90

Číst dál