Gestión de estado global sin dolor combinando Zod y Signals en aplicaciones modernas
Hace un par de años audité una aplicación enterprise en React y TypeScript que utilizaba Redux Toolkit. Para gestionar el estado de 6 pantallas principales, el equipo había tenido que escribir más de 3.500 líneas de código entre actions, reducers, selectors y Middlewares de Thunk.
Lo grave no era la cantidad de archivos. Lo grave era que cuando el backend cambiaba un campo opcional de la API sin avisar, el store de Redux aceptaba el objeto corrupto y la aplicación explotaba páginas más tarde con el temible Cannot read properties of undefined.
Habían creado un sistema complejo que no ofrecía ninguna protección real en tiempo de ejecución.
La combinación de Zod (validación de esquemas) y Signals (reactividad de grano fino) se ha convertido en el estándar moderno para eliminar el dolor de la gestión de estado global en aplicaciones frontend.
El problema de las librerías de estado tradicionales
Durante años creímos que para gestionar el estado de una aplicación web necesitábamos un contenedor monolítico global con patrones de inmutabilidad estrictos.
Ese enfoque sufría tres defectos estructurales:
- Verbosidad extrema: Escribir decenas de funciones de selección y mutación para actualizar una simple propiedad de usuario.
- Re-renderizados innecesarios: Si un componente escuchaba un objeto de estado global grande, cualquier cambio menor provocaba el re-renderizado del árbol de UI completo.
- Ceguera en la frontera API: Asumir que la respuesta del backend coincide al 100% con los tipos de TypeScript sin validar los datos entrantes.
Como destacamos en nuestro artículo sobre programación defensiva en TypeScript, las interfaces de TypeScript desaparecen al transpilar, por lo que confiar solo en tipos en tiempo de compilación es una trampa.
La Arquitectura Zod + Signals
La solución moderna consiste en aplicar la validación de esquemas en la frontera de entrada (HTTP) y gestionar la reactividad atómica mediante Signals (disponibles de forma nativa en Angular, Preact, SolidJS o mediante librerías ultraligeras como @preact/signals en React).
┌─────────────────────────────────────────────────────────┐
│ Respuesta API HTTP (JSON sin confiar) │
│ └─► Validacion en tiempo de ejecucion con Zod Schema │
│ ┌───────────────────────────────────────────────────┐
│ │ Estado Reactivo Atómico (Signals) │
│ │ └─► signal(), computed() │
│ └───────────────────────────────────────────────────┘
│ ┌──────────────────────────────────────────────────────┐
│ │ Componentes de UI (Actualización Quirúrgica) │
│ └──────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────┘
1. Definición del Esquema Zod y Tipado Automático
import { z } from 39;zod39;;
// 1. Esquema con validación estricta en tiempo de ejecución
export const UserStateSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
nombre: z.string().min(2),
rol: z.enum([39;ADMIN39;, 39;USER39;, 39;GUEST39;]),
preferencias: z.object({
tema: z.enum([39;light39;, 39;dark39;]).default(39;dark39;),
}),
});
// Inferir el tipo de TypeScript automáticamente
export type UserState = z.infer<typeof UserStateSchema>;
2. Store Reactivo basado en Signals
import { signal, computed } from 39;@preact/signals-react39;;
import { UserStateSchema, UserState } from 39;./user.schema39;;
// State atómico inicial
export const usuarioSignal = signal<UserState | null>(null);
export const estaAutenticadoSignal = computed(() => usuarioSignal.value !== null);
export const esAdminSignal = computed(() => usuarioSignal.value?.rol === 39;ADMIN39;);
// Acción de actualización con validación Zod defensiva
export function setUsuarioConValidacion(rawData: unknown) {
const parseResult = UserStateSchema.safeParse(rawData);
if (!parseResult.success) {
console.error(39;Payload de API inválido:39;, parseResult.error.format());
// Se evita corromper el estado global con datos inválidos
return false;
}
// Se asigna únicamente si la validación es 100% exitosa
usuarioSignal.value = parseResult.data;
return true;
}
Beneficios en Aplicaciones de Producción
- Re-renderizados quirúrgicos: Al consumir
esAdminSignalen un botón de administración, solo ese botón se re-evalúa cuando el rol cambia. El resto de la UI permanece intacta sin necesidad de memoizaciones manuales (useMemo,React.memo). - Cero corrupción de estado: Si la API devuelve un campo mal formateado, Zod detiene la propagación en la frontera HTTP antes de que afecte a la reactividad de la aplicación.
- Escalabilidad de código: Eliminas más del 70% del boilerplate de Redux/MobX, creando un código limpio que tanto los desarrolladores como los asistentes de IA pueden refactorizar sin riesgo.
Al estructurar los módulos de estado siguiendo los principios de graph engineering, consigues una separación clara entre la lógica de datos y los componentes de presentación.
Y si estás desarrollando en Angular, ten en cuenta el constante ciclo de releases de Angular donde los Signals y los Signal Forms se han integrado como el estándar nativo del framework.
Simplificar la gestión de estado combinando la solidez de Zod con la velocidad de los Signals permite construir interfaces mantenibles, reactivas y blindadas ante fallos de producción.
Si quieres dominar el desarrollo frontend moderno y las mejores prácticas de arquitectura con TypeScript, explora los Cursos de Dominicode. Y si quieres construir aplicaciones reales junto a otros desarrolladores senior, te esperamos en Dominicode Labs.
Preguntas frecuentes
¿Puedo usar Zod con otras librerías de estado como Zustand o Pinia?
Sí. Zod es una librería de validación agnóstica al framework. Puedes usar ZodSchema.parse() dentro de las acciones de Zustand, Pinia, Redux o cualquier otra librería para validar los datos antes de guardarlos en el store.
¿Qué diferencia hay entre la reactividad de Signals y los Observables de RxJS?
Los Signals están optimizados para la reactividad síncrona de UI con evaluación perezosa y seguimiento automático de dependencias. RxJS está diseñado para la coordinación de eventos asíncronos en el tiempo (peticiones HTTP, WebSockets, timers). En aplicaciones modernas, se usan Signals para el estado del componente y RxJS para streams asíncronos.
¿Zod añade demasiado peso al bundle del cliente?
No. Zod es una librería ultraligera (menos de 12 KB gzippeado) y soporta tree-shaking, por lo que solo se empaquetan en el cliente los métodos y validadores que utilices explícitamente en tu código.
¿Cómo persiste el estado basado en Signals entre recargas de página?
Puedes crear un efecto reactivo que sincronice automáticamente el valor del Signal con localStorage o sessionStorage cada vez que el Signal cambia, parseando los datos con Zod al restaurar la sesión.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
