Zpět na domů

typed-form-actions pro formuláře Next.js s Zod

Knihovna typed-form-actions zjednodušuje vytváření typizovaných formulářů v Next.js App Router. Automatizuje parsování FormData, validaci Zod a správu stavu React bez ztráty nativnosti HTML formulářů. Vhodná pro server-first přístup s minimálním klientským JS.

typed-form-actions: nativní typizované formuláře Next.js
Advertisement 728x90

typed-form-actions: typizované akce pro nativní formuláře v Next.js

Vývojáři Next.js často bojují s opakujícím se kódem při zpracování formulářů: parsování FormData, sbírání vnořených struktur, validace prostřednictvím Zod, převod chyb do formátu vhodného pro UI a správa stavu pomocí useActionState. Knihovna typed-form-actions tyto úkoly automatizuje, zároveň zachovává nativní HTML formuláře a přístup server-first.

Hlavní myšlenka spočívá v propojení FormData, Zod a useActionState do typizované vrstvy. Formuláře zůstávají standardními, ale získávají automatickou typizaci, zpracování chyb a stav pending bez nutnosti psát boilerplate.

Typická bolest bez abstrakce

V běžném scénáři s Server Action a Zod to vypadá takto:

Google AdInline article slot
  • Získání FormData z požadavku.
  • Ruční parsování polí do objektu.
  • Validace schématem.
  • Převod chyb Zod na Record<string, string[]>.
  • Návrat sjednoceného stavu s values, fieldErrors a formError.
  • Na klientovi – ruční přidání defaultValue, aria-invalid a zpracování chyb.

Příklad typického kódu:

"use server";
import { z } from "zod";

const contactSchema = z.object({
  name: z.string().min(2, "Jméno je příliš krátké."),
  email: z.string().email("Zadejte platný e-mail."),
  message: z.string().min(10, "Zpráva je příliš krátká."),
});

export async function sendMessage(prevState: any, formData: FormData) {
  const rawValues = {
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  };

  const parsed = contactSchema.safeParse(rawValues);
  if (!parsed.success) {
    return {
      status: "error",
      values: rawValues,
      data: null,
      formError: "Opravte zvýrazněná pole a zkuste to znovu.",
      fieldErrors: parsed.error.flatten().fieldErrors,
      submittedAt: Date.now(),
    };
  }
  // ...
}

Tento vzor se opakuje u každého formuláře. S poli, zaškrtávacími poli nebo vnořenými poli (profile.name, links[0].href) se kód rozrůstá exponenciálně.

Rozhraní knihovny

Knihovna je postavena na dvou funkcích:

Google AdInline article slot

createFormAction

Přijímá Zod-schéma a handler a vrátí připravený Server Action:

import { createFormAction } from "typed-form-actions";
import { z } from "zod";

const newsletterSchema = z.object({
  email: z.string().email("Zadejte platnou e-mailovou adresu."),
  topics: z.array(z.string()).min(1, "Vyberte alespoň jednu téma."),
  marketing: z
    .preprocess((value) => value === "on", z.boolean())
    .default(false),
});

export const subscribeAction = createFormAction({
  schema: newsletterSchema,
  async handler(values) {
    return {
      message: `Odebíráno ${values.email}`,
      topicsCount: values.topics.length,
    };
  },
});

useActionForm

Klientský hook nad useActionState s pohodlným rozhraním:

"use client";

import { useActionForm } from "typed-form-actions/react";

import { subscribeAction } from "./actions";

export function NewsletterForm() {
  const form = useActionForm(subscribeAction);

  return (
    <form action={form.action}>
      <label>
        Email
        <input {...form.getInputProps("email", { id: "email", type: "email" })} />
        {form.getFieldError("email") ? (
          <p id={form.getFieldErrorId("email")}>
            {form.getFieldError("email")}
          </p>
        ) : null}
      </label>

      <fieldset>
        <legend>Témata</legend>
        <label>
          <input
            {...form.getInputProps("topics", {
              type: "checkbox",
              value: "react",
            })}
          />
          React
        </label>
        {/* podobně pro ostatní zaškrtávací políčka */}
      </fieldset>

      {form.formError ? <p>{form.formError}</p> : null}

      <button disabled={form.isPending} type="submit">
        {form.isPending ? "Ukládání..." : "Odebírat"}
      </button>
    </form>
  );
}

Pod kapotou

Parsování FormData

Knihovna převádí rovinné FormData na objektovou strukturu:

Google AdInline article slot

Vstup:

{
  "profile.name": "Ada",
  "tags[]": ["react", "next"],
  "links[0].href": "/docs"
}

Výstup:

{
  profile: { name: "Ada" },
  tags: ["react", "next"],
  links: [{ href: "/docs" }]
}

Podporuje cesty jako profile.name, links[0].href a opakující se pole.

Unifikovaný FormActionState

type FormActionState<TValues, TResult> = {
  status: "idle" | "success" | "error";
  values: Partial<TValues>;
  data: TResult | null;
  fieldErrors: Record<string, string[]>;
  formError: string | null;
  submittedAt: number | null;
};

useActionForm automaticky generuje:

  • isPending
  • getFieldError(field)
  • getFieldErrorId(field)
  • defaultValue/defaultChecked
  • aria-invalid/aria-describedby

Porovnání s react-hook-form

| Aspekt | typed-form-actions | react-hook-form |

|--------|-------------------|-----------------|

| Nativní formuláře | ✅ | ❌ (na klientovi) |

| Server Actions | ✅ | Skrze adaptéry |

| Klientský JS | Minimální | Významný |

| Integrace Zod | Nativní | Skrze resolver |

| Složité formuláře | Základní podpora | Plná |

Knihovna neskonkuruje RHF, ale řeší problém server-first formulářů v Next.js App Router.

Současné možnosti

  • Parsování FormData, URLSearchParams, obyčejných objektů
  • Vnořené struktury a pole
  • Validace Zod
  • Předvídatelný FormActionState
  • React API pro chyby a stav pending
  • End-to-end typizace

Omezení

  • Žádný adapter pro react-hook-form
  • Složitá pole objektů vyžadují doplnění
  • Soubory a exotické vstupy nejsou podporovány
  • Rozhraní může být změněno

Co je důležité

  • Automatizace boilerplate: parsování, validace, chyby – bez ručního kódu
  • Nativnost: běžné <form>, žádné uncontrolled komponenty
  • Typizace: end-to-end od schématu po UI
  • Server-first: minimální klientský JS, maximální využití platformy
  • Předvídatelnost: jednotný FormActionState pro všechny formuláře

— Editorial Team

Advertisement 728x90

Číst dál