Gestión de estado en aplicaciones frontend modernas: guía práctica
La pregunta '¿qué librería de estado uso?' suele estar mal planteada. Antes hay que responder otra: ¿qué tipo de estado es este? Local, global o de servidor piden soluciones distintas, y la mitad de las veces la respuesta correcta es no añadir ninguna librería.
“¿Qué librería de estado uso?” es la pregunta que se hace la mayoría de equipos al arrancar un proyecto frontend, y es la pregunta equivocada hasta que se responde otra primero: ¿qué tipo de estado es este? No todo lo que vive en la memoria de una aplicación es lo mismo. Un valor que solo importa a un componente, un dato que varias partes de la interfaz necesitan compartir y una respuesta que viene de tu API no tienen el mismo ciclo de vida ni el mismo dueño, y tratarlos como si fueran el mismo problema es la razón por la que tantos proyectos terminan con un store global de trescientas líneas que en realidad es una caché de fetch mal disfrazada.
Tres tipos de estado, tres problemas distintos
Estado local es el que solo le importa a un componente (o a su árbol inmediato de hijos): si un modal está abierto, qué pestaña está activa, el valor de un input mientras el usuario escribe. Nace y muere con el componente. No necesita coordinación con el resto de la aplicación.
Estado global (o compartido) es el que varias partes desconectadas de la interfaz necesitan leer o modificar: el usuario autenticado, el tema claro/oscuro, el contenido de un carrito que se muestra tanto en el header como en la página de checkout. La característica que lo define no es “que sea importante”, sino que no hay un padre común razonable donde colocarlo sin forzar la estructura del árbol de componentes.
Estado de servidor es distinto a los dos anteriores en algo fundamental: no es tuyo. Es una copia local de datos que viven en otro sitio —una base de datos, una API de terceros— y que pueden quedar desactualizados en cualquier momento porque otro cliente, otro usuario o un proceso en segundo plano los cambió sin que tu aplicación se entere. Un array de productos, el perfil de un usuario, una lista de pedidos: todo esto es estado de servidor, y tratarlo como si fuera estado global normal (guardarlo en un store y ya) es la fuente más común de bugs de “datos obsoletos” en aplicaciones grandes.
Cuándo NO necesitas una librería de estado dedicada
La documentación oficial de React es clara en un punto que se pasa por alto con frecuencia: la mayoría de aplicaciones necesitan bastante menos “gestión de estado” de la que sus equipos terminan construyendo. Antes de instalar nada, vale la pena descartar, en orden:
- ¿Es un valor derivado de otro estado? Si
fullNamese puede calcular comofirstName + ' ' + lastNamedurante el renderizado, no es estado: es un cálculo. Guardarlo como estado aparte obliga a mantenerlo sincronizado a mano y es una fuente constante de bugs sutiles. - ¿De verdad lo necesitan varios componentes, o solo parece así? Antes de subir un valor al estado global, comprueba si “elevar el estado” (moverlo al padre común más cercano y pasarlo por props) resuelve el problema sin salir de React puro. Muchos “necesito estado global” son en realidad “no elevé el estado lo suficiente”.
- ¿Es solo paso de props a través de muchos niveles? Ahí el problema no es de gestión de estado, es de prop drilling, y la composición de componentes (pasar el componente hijo ya resuelto como prop, en lugar de reenviar datos nivel a nivel) suele resolverlo sin tocar ningún store.
- ¿Cabe en la URL? Filtros de una tabla, la pestaña activa, la página de una paginación: si el usuario esperaría que recargar la página o compartir el enlace preservara ese estado, probablemente pertenece a la URL (query params), no a un store en memoria. Librerías pequeñas como nuqs hacen esto tan ergonómico como un
useState, con el beneficio añadido de que el estado sobrevive a un refresco.
Cuando ya has descartado todo esto y sigue quedando un conjunto real de valores que múltiples partes desconectadas de la aplicación necesitan leer y escribir, useState combinado con Context puede bastar para aplicaciones pequeñas o medianas. El propio equipo de React documenta el patrón: un componente padre gestiona el estado con un reductor (useReducer) para las actualizaciones complejas, y Context lo distribuye a cualquier descendiente sin pasarlo prop a prop.
Donde ese patrón empieza a doler es en rendimiento: cualquier actualización del valor de un Context vuelve a renderizar a todos los componentes suscritos a él, aunque solo les interese una fracción del objeto que viaja por ese Context. En una aplicación pequeña no se nota. En un dashboard con cientos de componentes suscritos al mismo Context de “usuario y preferencias”, sí, y ahí es donde entra en juego una librería dedicada de estado.
Estado global: cuando sí hace falta una librería
Cuando el estado compartido crece en volumen o en frecuencia de actualización, una librería dedicada resuelve dos problemas que Context no resuelve bien por diseño: renderizados selectivos (que un componente solo se re-renderice si cambia la porción del estado que realmente usa) y una API más ergonómica para leer y mutar ese estado desde cualquier punto del árbol sin envolver media aplicación en proveedores anidados.
El panorama en 2026 se ha simplificado respecto a hace unos años, más que complicado. Redux, que dominó la década de 2015-2020 con su modelo estricto de acciones y reductores inmutables, sigue siendo una opción sólida para equipos grandes que valoran su disciplina explícita y sus herramientas de depuración (time-travel debugging, un ecosistema de middleware maduro), pero ha dejado de ser la opción por defecto: su ceremonia (actions, reducers, selectors, un store centralizado con configuración propia) es más peso del que la mayoría de aplicaciones necesita hoy.
La alternativa que se ha impuesto para el caso común es un modelo de store minimalista basado en hooks: defines un store con el estado y las funciones que lo modifican, y cualquier componente se suscribe solo a la porción que le interesa, sin proveedores, sin boilerplate de acciones tipadas a mano. Zustand es el ejemplo más extendido de este enfoque: un store se define en unas pocas líneas, cada componente decide qué “slice” del estado leer, y solo se re-renderiza cuando esa porción concreta cambia.
import { create } from 'zustand';
interface CarritoState {
items: string[];
añadir: (item: string) => void;
vaciar: () => void;
}
const useCarrito = create<CarritoState>((set) => ({
items: [],
añadir: (item) => set((state) => ({ items: [...state.items, item] })),
vaciar: () => set({ items: [] }),
}));
// En cualquier componente, sin Provider:
function ContadorCarrito() {
const items = useCarrito((state) => state.items);
return <span>{items.length} productos</span>;
}
Jotai representa una variante del mismo espíritu minimalista pero con un modelo distinto: en lugar de un store único con slices, defines átomos individuales de estado que se componen entre sí, un enfoque que encaja bien cuando el estado compartido es realmente un conjunto de piezas independientes más que un objeto único cohesionado.
Estado de servidor: el problema que ninguna librería de estado clásica resuelve bien
Aquí está el error más caro que se comete al diseñar la capa de estado de una aplicación: meter la respuesta de una API dentro del mismo store que gestiona el estado de la interfaz. Un store de Zustand o un reductor de Redux no saben, por diseño, cuándo ese dato se ha quedado obsoleto, si hay que volver a pedirlo, qué hacer si dos peticiones al mismo recurso se disparan a la vez, o cómo mostrar una actualización optimista mientras la petición real todavía está en vuelo.
Ese es exactamente el problema que resuelve una librería de estado de servidor. TanStack Query (antes React Query) es la referencia de esta categoría: no es un gestor de estado en el sentido de Redux o Zustand, es una capa de sincronización entre tu aplicación y el servidor que gestiona caché, revalidación en segundo plano, deduplicación de peticiones concurrentes y actualizaciones optimistas por ti.
import { useQuery } from '@tanstack/react-query';
function ListaProductos() {
const { data, isLoading, error } = useQuery({
queryKey: ['productos'],
queryFn: () => fetch('/api/productos').then((res) => res.json()),
staleTime: 60_000, // se considera "fresco" durante 60s
});
if (isLoading) return <p>Cargando...</p>;
if (error) return <p>Error al cargar productos</p>;
return <ul>{data.map((p: { id: string; nombre: string }) => <li key={p.id}>{p.nombre}</li>)}</ul>;
}
La propia documentación de TanStack Query es explícita sobre esto: no sustituye a Redux, Zustand ni a ningún otro gestor de estado de cliente, porque resuelve un problema distinto y complementario. La combinación habitual en 2026 —y la que evita la mayoría de bugs de sincronización— es TanStack Query (u otra librería equivalente) para todo lo que viene del servidor, y una librería ligera de estado de cliente (o simplemente useState/Context) para lo que es genuinamente estado de interfaz.
Este mismo problema aparece con otra cara en aplicaciones full-stack modernas: si tu framework ya resuelve mutaciones de servidor con Server Actions, buena parte de lo que antes exigía una librería de estado de servidor en el cliente se simplifica, porque la revalidación de datos ocurre como parte del propio flujo de la mutación en el servidor. No elimina la necesidad de gestionar estado de servidor en el cliente para lecturas, pero sí reduce cuánta lógica de sincronización manual necesitas escribir.
El panorama de opciones, sin listado plano
En lugar de una tabla de “librería A vs librería B vs librería C”, vale más entender los ejes reales de decisión:
Ceremonia frente a minimalismo. Redux Toolkit sigue siendo razonable si tu equipo es grande, necesita reglas explícitas sobre cómo se muta el estado y valora herramientas de depuración maduras (time-travel, un DevTools consolidado desde hace años). Zustand y Jotai priorizan lo contrario: menos estructura obligatoria, más velocidad para el caso común, al precio de menos convenciones impuestas por la propia librería (lo cual es una ventaja hasta que un equipo grande necesita esas convenciones para no pisarse).
Store único frente a átomos independientes. Un store de tipo Zustand modela bien estado que es naturalmente un objeto cohesionado (un carrito, una sesión). Un modelo de átomos como Jotai modela mejor estado compuesto de piezas que cambian de forma independiente y se combinan bajo demanda.
Reactividad fina (signals). Frameworks como Solid, Preact o Angular llevan tiempo usando signals: primitivas reactivas que actualizan directamente el DOM afectado sin pasar por un ciclo de re-render de componente completo, lo que en benchmarks reduce de forma notable el coste de actualizar UIs con muchas suscripciones pequeñas. Hay una propuesta en TC39 —todavía en fase 1, es decir, con la dirección acordada pero la API sin cerrar— para llevar signals al propio lenguaje JavaScript, respaldada por autores de Angular, Vue, Solid, Preact, Ember y Qwik entre otros, lo que sugiere hacia dónde tiende el ecosistema a medio plazo. React, de momento, no se construye sobre signals de forma nativa, así que adoptarlos hoy en un proyecto React significa salirse del modelo de renderizado que el resto del ecosistema (librerías de formularios, componentes de UI) asume por defecto.
Estado en la URL. Para cualquier estado que describe “qué está viendo el usuario ahora mismo” y que tendría sentido compartir por enlace, una librería como nuqs trata la URL como fuente de verdad en lugar de duplicar ese estado en memoria.
Una forma práctica de decidir
- ¿El valor solo lo usa un componente o su árbol inmediato? Estado local con
useStateouseReducer. No hace falta nada más. - ¿Es un cálculo a partir de otro estado? No lo guardes como estado; calcúlalo en el render.
- ¿Describe qué está viendo el usuario y tendría sentido compartirlo por URL? Query params, no un store.
- ¿Viene de tu API o de un servicio externo? Estado de servidor: TanStack Query o equivalente, nunca un store de cliente genérico.
- ¿Lo necesitan varios componentes desconectados, cambia poco y no es sensible al rendimiento de re-render? Context con
useReducerpuede bastar. - ¿Lo anterior empieza a notarse en rendimiento o en complejidad de proveedores anidados? Una librería de estado de cliente ligera (Zustand, Jotai) resuelve el problema con menos código del que parece.
El error más caro no es elegir Zustand en vez de Redux, o Jotai en vez de Zustand: esas decisiones son reversibles y el coste de migrar entre ellas es moderado. El error caro es no distinguir estado de servidor de estado de cliente desde el principio, porque esa mezcla se enquista en la arquitectura de la aplicación y se paga durante años en forma de bugs de datos obsoletos, condiciones de carrera entre peticiones y lógica de caché casera reinventada componente a componente.
Artículos relacionados
Web Components en 2026: ¿por fin listos para producción?
Custom Elements y Shadow DOM llevan una década siendo 'el futuro'. En 2026, con Declarative Shadow DOM y React 19 resolviendo la interoperabilidad, por fin hay una respuesta honesta a si están listos para producción: depende de qué estés construyendo.
Programación funcional en TypeScript: patrones prácticos sin dogmatismo
No hace falta un monad ni una librería de teoría de categorías para escribir TypeScript más predecible. Patrones funcionales que aportan claridad real, con código moderno y sin dogmatismo.
Bun vs Node.js vs Deno en 2026: qué runtime elegir y por qué
Tres runtimes de JavaScript, tres filosofías distintas. Esto es lo que dicen los datos (varios, no uno solo) y cuándo cada uno gana de verdad.