wandres.dev
TAXONOMÍA DEL ESTADO · local, global, servidor, URL

Formularios y la regla de oro

Los formularios son una clase de estado propia: valores, validación, touched, dirty y envío. Controlado frente a no controlado, las librerías de 2026 (React Hook Form, TanStack Form, useActionState) y la regla de oro que cierra la taxonomía: cada clase de estado pide su herramienta.

⏱ 16 min

Cerramos la taxonomía con el caso que lo reúne todo y con la regla que da sentido al nivel entero. Un formulario no es “unos cuantos useState”: es su propia clase de estado, con valores, errores de validación, campos tocados, estado sucio y fase de envío. Y una vez que ves las cinco clases —local, global de cliente, servidor, URL y formulario— la conclusión se impone sola: no todo va al store global. Cada clase tiene su tiempo de vida, su dueño y su herramienta. Esa es la regla de oro.

🎯 Al terminar esta lección sabrás
  • Ver el estado de formulario como una clase propia, no como estado local suelto.
  • Distinguir el enfoque controlado del no controlado y su coste en renders.
  • Conocer las herramientas de 2026: React Hook Form, TanStack Form y useActionState.
  • Formular la regla de oro: una herramienta por clase de estado, y el store para lo compartido.

El formulario es una clase de estado propia

Un formulario serio gestiona a la vez cinco cosas: los valores de cada campo, los errores de validación, qué campos se han tocado (touched), si el formulario está sucio (dirty, difiere del valor inicial) y en qué fase de envío está (idle, enviando, éxito, error). Modelar eso con useState sueltos degenera enseguida en un enredo de efectos y renders. Por eso el estado de formulario merece su propia entrada en la taxonomía: tiene una forma reconocible y herramientas dedicadas.

Hay además una decisión de fondo: controlado o no controlado. En un formulario controlado, cada tecla actualiza un useState y provoca un render; en uno no controlado, el DOM guarda el valor y solo lo lees al enviar (o vía ref). El controlado da control fino a costa de renders; el no controlado es rapidísimo y es la apuesta de las librerías modernas. La elección no es religiosa: necesitas control cuando la UI reacciona a cada pulsación (un contador de caracteres, un campo que habilita otro), y no controlado cuando el formulario solo importa al enviarse. La mayoría de los campos caen en el segundo grupo, y ahí pagar un render por tecla es puro desperdicio.

La forma se complica con los campos dinámicos: listas donde el usuario añade y quita filas (varias direcciones, líneas de un pedido). El estado deja de ser un objeto plano y pasa a ser una colección con identidad por fila, y librerías como React Hook Form ofrecen useFieldArray justamente para no reinventar esa gestión. Es la señal más clara de que el formulario es una clase con estructura propia, no un puñado de variables sueltas.

ℹ️
No controlado por defecto: por qué React Hook Form ganó

React Hook Form se volvió dominante justo por invertir el instinto: usa componentes no controlados y ref, de modo que teclear en un campo no re-renderiza todo el formulario. Solo re-renderiza cuando importa (al validar, al enviar). En formularios grandes la diferencia de rendimiento es abismal. La lección de diseño: el estado de formulario suele ser efímero y masivo en escrituras, y tratarlo como estado reactivo controlado es pagar un peaje en cada pulsación.

Las herramientas de 2026

El campo se reparte entre librerías especializadas y las nuevas primitivas del framework. React Hook Form y TanStack Form (headless, tipado de extremo a extremo, agnóstico de framework) cubren la validación rica con esquemas Zod o Valibot. En paralelo, React 19 trae los formularios como acciones, con useActionState para el ciclo de envío y pending integrado:

// React 19: el formulario como accion + estado de envio integrado
function Alta() {
  const [estado, formAction, pending] = useActionState(crearUsuario, null);
  return (
    <form action={formAction}>
      <input name="email" />
      <button disabled={pending}>Crear</button>
      {estado?.error && <p role="alert">{estado.error}</p>}
    </form>
  );
}

Fíjate en la mezcla de clases: los valores del formulario los gestiona el <form> (no controlado, vía FormData), el estado de envío lo da useActionState, y crearUsuario acaba escribiendo en el servidor — cuya cache habrá que invalidar (lección 3). Un formulario real toca varias clases de estado a la vez; el arte está en dar cada parte a su herramienta y no forzarlas a un único cajón.

Cuando el formulario tiene validación rica y muchos campos, una librería dedicada gana. React Hook Form combina componentes no controlados con un resolver de esquema:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const { register, handleSubmit, formState } = useForm<Alta>({
  resolver: zodResolver(esquemaAlta),
});
// register conecta cada input sin re-render por tecla;
// formState trae errors, isDirty e isSubmitting listos para usar

Validación: esquemas y el borde del servidor

La validación es el corazón del estado de formulario, y su forma canónica en 2026 es un esquema declarativo (Zod, Valibot) que actúa como fuente única de la verdad para el tipo y las reglas a la vez:

import { z } from 'zod';

const esquemaAlta = z.object({
  email: z.string().email('Email no valido'),
  edad: z.number().int().min(18, 'Debes ser mayor de edad'),
});
type Alta = z.infer<typeof esquemaAlta>; // el tipo se deriva del esquema

La disciplina que no se debe olvidar: la validación de cliente es para la experiencia (feedback inmediato), no para la seguridad. El cliente es manipulable, así que el mismo esquema debe volver a validarse en el servidor —en la Server Action o el endpoint— antes de tocar la base de datos. Compartir un único esquema entre cliente y servidor, posible cuando ambos son TypeScript, elimina la duplicación y garantiza que las reglas nunca diverjan.

📝
El estado de formulario es efímero por diseño

Un error tentador es querer “guardar” el estado del formulario en un store global o persistirlo. Salvo el caso concreto de un borrador que el usuario pide recuperar, el estado de formulario nace al abrir el formulario y muere al enviarlo: es efímero, como el local, pero con estructura propia. No pertenece al store global, no pertenece a la URL (los valores a medio escribir no son navegables ni compartibles) y no pertenece al servidor hasta que se confirma. Es su propia clase precisamente porque no encaja limpiamente en ninguna otra.

La regla de oro

Con las cinco clases sobre la mesa, la taxonomía deja de ser descriptiva y se vuelve una herramienta de decisión. Ante cualquier dato, recórrelo por el mismo árbol: ¿viene del servidor?, ¿debe ser navegable?, ¿lo comparten varias vistas?, ¿pertenece a un formulario? Lo que sobreviva a las cuatro preguntas —y solo eso— es estado local.

flowchart TD
S[un dato en tu app] --> Q1[viene de un servidor remoto]
Q1 -->|si| SV[cache de servidor: TanStack Query]
Q1 -->|no| Q2[debe compartirse por enlace o sobrevivir a recargar]
Q2 -->|si| URL[estado de URL: search params]
Q2 -->|no| Q3[lo lee mas de una vista lejana]
Q3 -->|si| GL[global de cliente: store]
Q3 -->|no| Q4[pertenece a un formulario]
Q4 -->|si| FM[estado de formulario: RHF o Form]
Q4 -->|no| LO[estado local: useState]
style SV fill:#fab387,color:#11111b
style URL fill:#89b4fa,color:#11111b
style GL fill:#cba6f7,color:#11111b
style FM fill:#f9e2af,color:#11111b
style LO fill:#a6e3a1,color:#11111b
🍃

Local de UI

Efímero, de un componente. Herramienta: useState, useReducer, useRef.

🌍

Global de cliente

Compartido de verdad entre vistas. Herramienta: un store (Zustand, Jotai) o Contexto.

🛰️

Del servidor

Verdad remota, es una cache. Herramienta: TanStack Query, SWR, RSC.

🔗

De la URL

Navegable y compartible. Herramienta: URLSearchParams, nuqs, router tipado.

📝

De formulario

Valores, validación y envío. Herramienta: React Hook Form, TanStack Form.

Una herramienta por clase; el store no es un cajón de sastre

La regla de oro que cierra este nivel se enuncia en una línea: identifica primero a qué clase pertenece cada estado, y solo entonces elige la herramienta — porque cada clase tiene un dueño, un tiempo de vida y una disciplina distintos, y forzarlas todas al mismo store global es la causa raíz de la mayoría de las arquitecturas de estado que envejecen mal. El razonamiento es el mismo que atraviesa todo el track. El estado local muere con su vista y no necesita salir de un archivo. El global de cliente es una verdad compartida que unas pocas vistas leen, y su tamaño debe ser diminuto y celosamente vigilado. El del servidor no es tuyo: es una cache de una verdad ajena, con política de frescura y revalidación, y meterlo en el store es reescribir mal una cache. El de la URL pertenece al usuario, que lo comparte y lo recarga, y esconderlo en memoria es negarle capacidades que la plataforma da gratis. El de formulario es efímero, masivo en escrituras y con una forma propia, y merece una librería que no re-renderice el mundo en cada tecla. Cinco clases, cinco herramientas, cinco disciplinas. El anti-patrón que unifica a todos los proyectos que sufren es el mismo: un único store global convertido en cajón de sastre, donde conviven el booleano de un modal, la respuesta de una API, el filtro de una tabla y los campos de un formulario — todo mezclado, todo global, todo obsoleto a la vez. El buen diseño de estado no consiste en elegir la librería de moda, sino en clasificar con honestidad antes de guardar. Cuando cada dato vive en la clase que le corresponde, los bugs de sincronización se evaporan, el store adelgaza hasta lo esencial, y la aplicación se vuelve legible: cada estado dice, por dónde vive, qué es y a quién pertenece. Esa es la maestría de la taxonomía — no saber más herramientas, sino saber cuál no usar.

⚔️ Aplica la regla de oro a una app real
  1. Elige una pantalla con formulario y clasifica cada dato en una de las cinco clases antes de tocar código.
  2. Reconstruye el formulario con React Hook Form o useActionState y comprueba que teclear no re-renderiza toda la pantalla.
  3. Recorre el árbol de decisión del diagrama para cada estado de la pantalla y confirma que está en la herramienta correcta.
  4. Audita tu store global: saca de él todo lo que sea servidor, URL, formulario o local; anota cuánto adelgaza.
  5. Comparte un mismo esquema Zod entre la validación de cliente y la del servidor, y verifica que las reglas no se duplican.