typed-form-actions: Acciones de formularios seguras con tipos para formularios nativos en Next.js
Los desarrolladores de Next.js a menudo enfrentan código repetitivo al manejar formularios: analizar FormData, construir estructuras anidadas, validar con Zod, transformar errores para la interfaz de usuario y gestionar useActionState. La biblioteca typed-form-actions automatiza estas tareas manteniendo los formularios HTML nativos y un enfoque centrado en el servidor.
La idea central es unificar FormData, Zod y useActionState en una capa segura con tipos. Los formularios permanecen estándar, pero ganan tipado automático, manejo de errores y estado pendiente sin boilerplate.
El dolor sin abstracción
En un flujo típico de Acción del Servidor con Zod, verías:
- Extraer FormData de la solicitud.
- Analizar manualmente los campos en un objeto.
- Validar contra un esquema.
- Convertir errores de Zod en
Record<string, string[]>. - Devolver un estado estandarizado con
values,fieldErrorsyformError. - En el cliente: vincular manualmente
defaultValue,aria-invalidy manejar errores.
Ejemplo de código típico:
"use server";
import { z } from "zod";
const contactSchema = z.object({
name: z.string().min(2, "El nombre es demasiado corto."),
email: z.string().email("Introduce un correo válido."),
message: z.string().min(10, "El mensaje es demasiado corto."),
});
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: "Por favor, corrige los campos resaltados e inténtalo de nuevo.",
fieldErrors: parsed.error.flatten().fieldErrors,
submittedAt: Date.now(),
};
}
// ...
}
Este patrón se repite en cada formulario. Con arreglos, casillas de verificación o campos anidados (como profile.name, links[0].href), el código crece exponencialmente.
API de la biblioteca
La biblioteca se basa en dos funciones clave:
createFormAction
Acepta un esquema de Zod y un manejador, y devuelve una Acción del Servidor lista para usar:
import { createFormAction } from "typed-form-actions";
import { z } from "zod";
const newsletterSchema = z.object({
email: z.string().email("Introduce una dirección de correo válida."),
topics: z.array(z.string()).min(1, "Selecciona al menos un tema."),
marketing: z
.preprocess((value) => value === "on", z.boolean())
.default(false),
});
export const subscribeAction = createFormAction({
schema: newsletterSchema,
async handler(values) {
return {
message: `Suscripto ${values.email}`,
topicsCount: values.topics.length,
};
},
});
useActionForm
Un hook del lado del cliente basado en useActionState con una API limpia:
"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>
Correo electrónico
<input {...form.getInputProps("email", { id: "email", type: "email" })} />
{form.getFieldError("email") ? (
<p id={form.getFieldErrorId("email")}>
{form.getFieldError("email")}
</p>
) : null}
</label>
<fieldset>
<legend>Temas</legend>
<label>
<input
{...form.getInputProps("topics", {
type: "checkbox",
value: "react",
})}
/>
React
</label>
{/* Similar para otras casillas */}
</fieldset>
{form.formError ? <p>{form.formError}</p> : null}
<button disabled={form.isPending} type="submit">
{form.isPending ? "Guardando..." : "Suscribirse"}
</button>
</form>
);
}
Detrás de escena
Análisis de FormData
La biblioteca transforma FormData plano en objetos anidados:
Entrada:
{
"profile.name": "Ada",
"tags[]": ["react", "next"],
"links[0].href": "/docs"
}
Salida:
{
profile: { name: "Ada" },
tags: ["react", "next"],
links: [{ href: "/docs" }]
}
Soporta rutas como profile.name, links[0].href y campos repetidos.
FormActionState unificado
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 genera automáticamente:
isPendinggetFieldError(field)getFieldErrorId(field)defaultValue/defaultCheckedaria-invalid/aria-describedby
Comparación con react-hook-form
| Aspecto | typed-form-actions | react-hook-form |
|--------|-------------------|-----------------|
| Formularios nativos | ✅ | ❌ (del lado del cliente) |
| Acciones del servidor | ✅ | A través de adaptadores |
| JavaScript del cliente | Mínimo | Significativo |
| Integración con Zod | Nativa | A través de resolver |
| Formularios complejos | Soporte básico | Soporte completo |
Esta biblioteca no compite con RHF; resuelve el problema de formularios centrados en el servidor en Next.js App Router.
Características actuales
- Analiza FormData, URLSearchParams y objetos planos
- Estructuras anidadas y arreglos
- Validación con Zod
- FormActionState predecible
- API de React para errores y estados pendientes
- Tipado end-to-end
Limitaciones
- Sin adaptador para react-hook-form
- Arreglos de objetos complejos requieren refinamiento
- Archivos e inputs exóticos no cubiertos
- La API puede cambiar
Lo que más importa
- Automatización de boilerplate: análisis, validación, errores — sin código manual
- Formularios nativos: elementos
<form>estándar, sin componentes no controlados - Seguridad de tipos: desde el esquema hasta la interfaz de usuario
- Enfoque centrado en el servidor: mínimo JavaScript del cliente, máximo beneficio de la plataforma
- Previsibilidad: FormActionState consistente en todos los formularios
— Editorial Team
Aún no hay comentarios.