typed-form-actions : Actions de formulaire sécurisées par type pour les formulaires natifs dans Next.js
Les développeurs Next.js sont souvent confrontés à du code répétitif lors de la gestion des formulaires : extraction de FormData, construction de structures imbriquées, validation avec Zod, transformation des erreurs pour l’interface utilisateur et gestion de useActionState. La bibliothèque typed-form-actions automatiser ces tâches tout en préservant les formulaires HTML natifs et une approche server-first.
L'idée centrale est de regrouper FormData, Zod et useActionState dans une couche sécurisée par types. Les formulaires restent standards, mais bénéficient d'une typage automatique, d'une gestion d'erreurs et d'un état de chargement — sans aucune surcharge.
La douleur sans abstraction
Dans un flux classique d'Action serveur avec Zod, on retrouve :
- Extraction de FormData à partir de la requête.
- Analyse manuelle des champs vers un objet.
- Validation selon un schéma.
- Conversion des erreurs Zod en
Record<string, string[]>. - Retour d'un état standardisé avec
values,fieldErrorsetformError. - Sur le client : liaison manuelle de
defaultValue,aria-invalidet gestion des erreurs.
Exemple de code typique :
"use server";
import { z } from "zod";
const contactSchema = z.object({
name: z.string().min(2, "Le nom est trop court."),
email: z.string().email("Entrez un e-mail valide."),
message: z.string().min(10, "Le message est trop court."),
});
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: "Veuillez corriger les champs mis en évidence et réessayer.",
fieldErrors: parsed.error.flatten().fieldErrors,
submittedAt: Date.now(),
};
}
// ...
}
Ce schéma se répète à chaque formulaire. Avec des tableaux, cases à cocher ou champs imbriqués (comme profile.name, links[0].href), le code augmente exponentiellement.
API de la bibliothèque
La bibliothèque repose sur deux fonctions clés :
createFormAction
Accepte un schéma Zod et un handler, retourne une Action serveur prête à l'emploi :
import { createFormAction } from "typed-form-actions";
import { z } from "zod";
const newsletterSchema = z.object({
email: z.string().email("Entrez une adresse e-mail valide."),
topics: z.array(z.string()).min(1, "Sélectionnez au moins un sujet."),
marketing: z
.preprocess((value) => value === "on", z.boolean())
.default(false),
});
export const subscribeAction = createFormAction({
schema: newsletterSchema,
async handler(values) {
return {
message: `Inscrit ${values.email}`,
topicsCount: values.topics.length,
};
},
});
useActionForm
Un hook côté client basé sur useActionState, avec une API simple :
"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>Sujets</legend>
<label>
<input
{...form.getInputProps("topics", {
type: "checkbox",
value: "react",
})}
/>
React
</label>
{/* Similaire pour les autres cases à cocher */}
</fieldset>
{form.formError ? <p>{form.formError}</p> : null}
<button disabled={form.isPending} type="submit">
{form.isPending ? "Enregistrement..." : "S'abonner"}
</button>
</form>
);
}
Fonctionnement interne
Analyse de FormData
La bibliothèque transforme FormData plat en objets imbriqués :
Entrée :
{
"profile.name": "Ada",
"tags[]": ["react", "next"],
"links[0].href": "/docs"
}
Sortie :
{
profile: { name: "Ada" },
tags: ["react", "next"],
links: [{ href: "/docs" }]
}
Prend en charge des chemins comme profile.name, links[0].href et les champs répétés.
FormActionState unifié
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 génère automatiquement :
isPendinggetFieldError(field)getFieldErrorId(field)defaultValue/defaultCheckedaria-invalid/aria-describedby
Comparaison avec react-hook-form
| Aspect | typed-form-actions | react-hook-form |
|--------|-------------------|-----------------|
| Formulaires natifs | ✅ | ❌ (côté client) |
| Actions serveur | ✅ | Via adaptateurs |
| JavaScript client | Minimal | Important |
| Intégration Zod | Native | Via resolver |
| Formulaires complexes | Support basique | Support complet |
Cette bibliothèque ne concurrence pas RHF — elle résout le problème des formulaires orientés serveur dans Next.js App Router.
Fonctionnalités actuelles
- Analyse de FormData, URLSearchParams, objets plats
- Structures imbriquées et tableaux
- Validation Zod
- FormActionState prévisible
- API React pour les erreurs et l'état de chargement
- Typage end-to-end
Limites
- Aucun adaptateur pour react-hook-form
- Tableaux d'objets complexes nécessitent un affinement
- Fichiers et entrées exotiques non couverts
- L'API peut évoluer
Ce qui compte le plus
- Automatisation de la boilerplate : analyse, validation, erreurs — pas de code manuel
- Formulaires natifs : éléments
<form>standards, pas de composants non contrôlés - Sécurité par type : de la définition du schéma à l’interface utilisateur
- Approche server-first : peu de JavaScript client, maximum de bénéfices plateforme
- Prévisibilité : FormActionState cohérent sur tous les formulaires
— Editorial Team
Aucun commentaire pour le moment.