Powrót do strony głównej

typed-form-actions dla formularzy Next.js z Zod

Biblioteka typed-form-actions upraszcza tworzenie typizowanych formularzy w Next.js App Router. Automatyzuje parsowanie FormData, walidację Zod i zarządzanie stanem React bez utraty natywności formularzy HTML. Nadaje się do podejścia server-first z minimalnym JS po stronie klienta.

typed-form-actions: natywne typizowane formularze Next.js
Advertisement 728x90

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:

Google AdInline article slot
  • 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:

Google AdInline article slot

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ą:

Google AdInline article slot

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:

  • isPending
  • getFieldError(field)
  • getFieldErrorId(field)
  • defaultValue/defaultChecked
  • aria-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

Advertisement 728x90

Czytaj dalej