typed-form-actions: typizowane akcje dla formularzy w Next.js
Rozwój aplikacji w Next.js często wiąże się z powtarzalnym kodem podczas obsługi formularzy: parsowanie FormData, budowanie zagnieżdżonych struktur, walidacja za pomocą Zod, konwersja błędów do formatu UI oraz zarządzanie stanem useActionState. Biblioteka typed-form-actions automatyzuje te zadania, zachowując naturalne formularze HTML i podejście server-first.
Główna idea polega na połączeniu FormData, Zod i useActionState w jednolity warstwę typów. Formularze pozostają standardowe, ale otrzymują automatyczną typizację, obsługę błędów i stan pending bez zbędnych fragmentów kodu.
Typowa trudność bez abstrakcji
W standardowym scenariuszu Server Action z Zod wygląda to tak:
- Pobranie FormData z żądania.
- Ręczne przetworzenie pól w obiekt.
- Walidacja schematu.
- Konwersja błędów Zod na Record<string, string[]>.
- Zwrócenie znormalizowanego stanu z values, fieldErrors i formError.
- Na kliencie — ręczna powiązanie defaultValue, aria-invalid i obsługa błędów.
Przykład typowego kodu:
"use server";
import { z } from "zod";
const contactSchema = z.object({
name: z.string().min(2, "Imię jest zbyt krótkie."),
email: z.string().email("Podaj poprawny adres e-mail."),
message: z.string().min(10, "Wiadomość jest zbyt krótka."),
});
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: "Popraw pola zaznaczone i spróbuj ponownie.",
fieldErrors: parsed.error.flatten().fieldErrors,
submittedAt: Date.now(),
};
}
// ...
}
Ten wzorzec powtarza się w każdej formularzu. Wraz z tablicami, polami wyboru lub zagnieżdżonymi polami (profile.name, links[0].href) kod rośnie wykładniczo.
Interfejs biblioteki
Biblioteka opiera się na dwóch funkcjach:
createFormAction
Przyjmuje schemat Zod i handler, zwraca gotowy Server Action:
import { createFormAction } from "typed-form-actions";
import { z } from "zod";
const newsletterSchema = z.object({
email: z.string().email("Podaj poprawny adres e-mail."),
topics: z.array(z.string()).min(1, "Wybierz co najmniej jedną tematykę."),
marketing: z
.preprocess((value) => value === "on", z.boolean())
.default(false),
});
export const subscribeAction = createFormAction({
schema: newsletterSchema,
async handler(values) {
return {
message: `Zapisano ${values.email}`,
topicsCount: values.topics.length,
};
},
});
useActionForm
Hook kliencki oparty na useActionState z przyjaznym interfejsem:
"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>Tematyki</legend>
<label>
<input
{...form.getInputProps("topics", {
type: "checkbox",
value: "react",
})}
/>
React
</label>
{/* podobnie dla innych checkboxów */}
</fieldset>
{form.formError ? <p>{form.formError}</p> : null}
<button disabled={form.isPending} type="submit">
{form.isPending ? "Zapisywanie..." : "Zapisz się"}
</button>
</form>
);
}
Pod kapotą
Parsowanie FormData
Biblioteka konwertuje płaskie FormData w strukturę obiektową:
Wejście:
{
"profile.name": "Ada",
"tags[]": ["react", "next"],
"links[0].href": "/docs"
}
Wyjście:
{
profile: { name: "Ada" },
tags: ["react", "next"],
links: [{ href: "/docs" }]
}
Obsługuje ścieżki typu profile.name, links[0].href, powtarzające się pola.
Unifikowany 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 automatycznie generuje:
isPendinggetFieldError(field)getFieldErrorId(field)defaultValue/defaultCheckedaria-invalid/aria-describedby
Porównanie z react-hook-form
| Aspekt | typed-form-actions | react-hook-form |
|--------|-------------------|-----------------|
| Formularze natywne | ✅ | ❌ (po stronie klienta) |
| Server Actions | ✅ | Przez adaptery |
| JavaScript klienta | Minimalny | Znaczny |
| Integracja Zod | Naturalna | Przez resolver |
| Skomplikowane formularze | Podstawowa obsługa | Pełna |
Biblioteka nie konkurowa z RHF, a rozwiązuje problem formularzy server-first w Next.js App Router.
Obecne możliwości
- Parsowanie FormData, URLSearchParams, prostych obiektów
- Zagnieżdżone struktury i tablice
- Walidacja Zod
- Przewidywalny FormActionState
- API React do błędów i stanu pending
- Typizacja end-to-end
Ograniczenia
- Brak adaptera do react-hook-form
- Skomplikowane tablice obiektów wymagają ulepszeń
- Pliki i egzotyczne pola nie są obsługiwane
- Interfejs może ulec zmianie
Co jest ważne
- Automatyzacja boilerplate: parsowanie, walidacja, błędy — bez ręcznego kodowania
- Natywność: zwykłe
<form>, żadnych komponentów niekontrolowanych - Typizacja: end-to-end od schematu do interfejsu
- Server-first: minimalny kod JS po stronie klienta, maksymalne wykorzystanie możliwości platformy
- Przewidywalność: jednolity FormActionState dla wszystkich formularzy
— Editorial Team
Brak komentarzy.