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:
- 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:
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:
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:
isPendinggetFieldError(field)getFieldErrorId(field)defaultValue/defaultCheckedaria-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
Zatím žádné komentáře.