El patrón dominante de 2026
El stack que reemplazó al Redux monolítico no es una librería sino una división de trabajo por clase de estado: TanStack Query gobierna el estado del servidor, Zustand el estado global de cliente y Jotai el estado de formularios y derivado. Esta lección muestra las tres clases con su dueño idiomático, explica por qué la división derrotó al store único —el estado del servidor, la porción mayor y más difícil, se fue a una cache especializada—, traza las fronteras que no deben cruzarse para no duplicar un mismo dato en dos capas, y recorre un ejemplo integrado donde las tres herramientas conviven sin pisarse. El principio que las une es el mismo de todo el track: la herramienta debe ajustarse a la clase de estado, y unificarlas fue el error que este patrón corrige.
Las tres lecciones anteriores parecían ofrecer alternativas rivales a Redux: usa Redux Toolkit, o Zustand, o Jotai. La lección de hoy revela que la pregunta estaba mal planteada, porque en las aplicaciones reales de 2026 la respuesta dominante no es elegir una, sino usar tres a la vez, cada una para una clase distinta de estado. El estado del servidor va a TanStack Query, el estado global de cliente a Zustand, el estado de formularios y derivado a Jotai. No compiten: se reparten el trabajo. Este patrón —una división de labores por clase de estado, en vez de un único store que lo absorbe todo— es la arquitectura por defecto que sustituyó al Redux monolítico de la década anterior, y entenderlo como un sistema, y no como tres herramientas sueltas, es el objetivo de este nivel.
- Ver las tres clases de estado con su herramienta idiomática de 2026 y por qué no se solapan.
- Entender por qué la división de trabajo derrotó al store monolítico único.
- Trazar las fronteras del patrón: qué dato no debe vivir nunca en dos capas.
- Leer un ejemplo integrado donde
TanStack Query,ZustandyJotaiconviven sin pisarse.
Tres clases de estado, tres dueños
El movimiento intelectual que hace posible este patrón es dejar de preguntar qué librería de estado usar y empezar a preguntar de qué clase es este estado. Porque hay clases distintas, con propiedades distintas, y cada una tiene un dueño idiomático. El estado del servidor es una copia de una verdad remota —una cache con obsolescencia y revalidación— y su dueño es TanStack Query. El estado global de cliente es una verdad que posee el cliente y que muchos componentes leen —tema, sesión, banderas de UI— y su dueño es Zustand. El estado de formularios y derivado es una red de valores interdependientes y de grano fino, y su dueño es Jotai.
Servidor → TanStack Query
Datos remotos: listas, perfiles, resultados de búsqueda. Una cache con staleTime, revalidación, deduplicación e invalidación. Nunca es estado propio.
Cliente global → Zustand
Verdad del cliente que muchos leen: tema, sesión, si un panel está abierto, qué está seleccionado. Un store plano con selectores de grano fino.
Formularios y derivado → Jotai
Grafos de dependencias: campos que se validan entre sí, totales computados, celdas. Átomos que se recomputan solos, aislados por atomFamily.
Local y URL → useState y router
Lo que solo usa un componente vive en useState; lo compartible o navegable —filtros, pestaña activa— vive en la URL, con nuqs o el router.
Conviene resistir la tentación de tratar esas dos últimas clases —local y URL— como ciudadanas de segunda. El estado en useState no es un fracaso por no ser global: es la respuesta correcta cuando de verdad solo un componente lo usa, y promoverlo a un store es acoplar sin motivo. Y la URL no sirve solo para navegar: un filtro, una pestaña o una búsqueda guardados en ella son estado compartible por enlace, restaurable al recargar y recorrible con el botón atrás, tres propiedades que un store jamás te da gratis. Buena parte de lo que acaba en Zustand pertenecía en realidad a la URL.
La consecuencia de este reparto es liberadora: ninguna de las tres herramientas tiene que ser buena en todo, porque ninguna carga con todo. TanStack Query puede especializarse en el problema difícil de la cache sin preocuparse por el estado de cliente; Zustand puede ser diminuto porque no tiene que resolver la revalidación; Jotai puede optimizar la derivación fina sin cargar con la persistencia de sesión. Cada una es pequeña y afilada porque su ámbito es estrecho.
Por qué la división derrotó al monolito
Para entender por qué este patrón desplazó a Redux hay que ver el defecto estructural del store único. Un store monolítico es un solo cajón para clases de estado con dificultades radicalmente distintas, y esa unificación tiene un coste oculto: la herramienta debe ser lo bastante estricta para el caso más difícil —sincronizar una cache de servidor— y esa misma estrictez se paga también para el caso más fácil —guardar un booleano—. Redux obligaba a despachar una acción, escribir un reducer y un selector tanto para invalidar una cache remota como para alternar un tema. Pagabas el precio del problema difícil en todos los problemas.
La división rompe ese acoplamiento: cada clase usa una herramienta con el precio justo de su dificultad. Y aquí está el dato central de 2026 que explica el declive de Redux: la porción más grande y más difícil del estado de una app típica es el estado del servidor, y TanStack Query se la llevó entera. Al extraer esa porción hacia una cache especializada, lo que quedaba para el store de cliente resultó ser tan poco y tan simple —unas banderas, una sesión— que ya no justificaba la maquinaria de Redux. Redux no perdió una batalla de features: perdió su razón de ser cuando su carga principal, el servidor, encontró un dueño mejor.
Se objeta a veces que tres herramientas son más complejas que una. La confusión está en medir la complejidad por el número de dependencias en vez de por la claridad de las responsabilidades. Un store monolítico tiene una dependencia y mil responsabilidades entremezcladas; el stack dividido tiene tres dependencias y tres responsabilidades nítidas, cada una con su frontera evidente. La segunda forma es más simple de razonar aunque sume más paquetes al package.json, porque la simplicidad que importa es la del modelo mental, no la del árbol de dependencias.
flowchart TD APP[una aplicacion tipica de 2026] --> SRV[estado de servidor] APP --> CLI[estado de cliente global] APP --> FRM[estado de formularios y derivado] SRV --> TQ[TanStack Query] CLI --> ZU[Zustand] FRM --> JO[Jotai] style SRV fill:#f38ba8,color:#11111b style TQ fill:#f38ba8,color:#11111b style ZU fill:#a6e3a1,color:#11111b style JO fill:#89b4fa,color:#11111b
Que Redux Toolkit respondiera con RTK Query —una cache de servidor pegada al store— es la prueba indirecta de esta tesis: hasta el campeón del monolito reconoció que el estado del servidor era una clase aparte y le dio maquinaria propia. La diferencia es que el stack dominante lleva esa lógica a su conclusión y usa la mejor herramienta para cada clase sin obligarlas a compartir store, mientras que RTK Query mantiene la cache dentro de Redux para quien ya paga su disciplina por otras razones. Ambos aceptan la taxonomía; discrepan solo en si vale la pena un store común.
Las fronteras: qué no duplicar
La disciplina de este patrón no está en las tres herramientas, sino en las fronteras entre ellas. Casi todos los bugs de esta arquitectura son violaciones de frontera: el mismo dato viviendo en dos capas a la vez, condenado a desincronizarse. La regla es que cada dato tiene un único dueño, y las otras capas lo leen de él, no lo copian.
El error más repetido al montar este stack es traer una lista con TanStack Query y, acto seguido, guardarla en Zustand para tenerla a mano. En ese instante el dato vive en dos sitios: la cache de Query, que sabe cuándo caduca y cómo revalidar, y una copia en el store, que no sabe nada de eso y quedará obsoleta en cuanto la verdad remota cambie. Has recreado, a mano y peor, el anti-patrón que hundió a Redux. La regla no admite excepción cómoda: el dato de servidor vive en la cache y se lee desde ella con useQuery; si otro componente lo necesita, vuelve a llamar a useQuery con la misma queryKey —que deduplica— en lugar de copiarlo al store.
Las otras dos fronteras son igual de nítidas. No subas a Zustand un estado de formulario que solo usa un formulario: mientras viva en un componente o en átomos locales, su ámbito es su ámbito, y globalizarlo es crear acoplamiento sin motivo. Y no derives en un store lo que un átomo deriva mejor: si un total depende de tres campos, es una derivación de Jotai, no un valor que recalculas dentro de una acción de Zustand. Cada frontera responde a la misma pregunta —quién es el dueño de este dato— y respetarlas es lo que mantiene el sistema sin contradicciones.
Respetar la frontera del servidor es más fácil de lo que el miedo a repetir peticiones sugiere, porque la deduplicación por clave lo hace gratis:
// dos componentes lejanos piden el mismo dato: una sola peticion, sin copiarlo a un store
function Cabecera() {
const { data } = useQuery({ queryKey: ['perfil'], queryFn: traerPerfil })
return <span>{data?.nombre}</span>
}
function Ajustes() {
const { data } = useQuery({ queryKey: ['perfil'], queryFn: traerPerfil })
return <Formulario perfil={data} />
}
Ambos leen ['perfil'] de la misma cache; TanStack Query deduplica la petición y comparte el resultado. La clave es la identidad del dato, como en el Nivel 32. Copiar ese perfil a Zustand para compartirlo entre los dos componentes no solo es innecesario: reintroduce el problema de sincronización que la deduplicación por clave ya había resuelto.
Un ejemplo integrado
Nada aclara el patrón como ver las tres capas conviviendo en una sola pantalla. Toma un checkout de comercio electrónico. La lista de productos y su stock viven en el servidor: son una cache. El tema y la sesión los posee el cliente: van a Zustand. El formulario de pago, con sus campos que se validan entre sí y su total con envío que se deriva de varios valores, es un grafo: va a Jotai. Cada línea del componente deja claro de qué capa lee, y ninguna capa invade a otra.
import { useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useSesion } from './stores/sesion'
import { totalConEnvioAtom } from './atoms/checkout'
function Checkout() {
// servidor: los productos son una cache de una verdad remota
const { data: productos } = useQuery({ queryKey: ['productos'], queryFn: traerProductos })
// cliente global: el tema lo posee el cliente y muchos componentes lo leen
const tema = useSesion((s) => s.tema)
// formulario y derivado: el total con envio es un grafo de atomos interdependientes
const total = useAtomValue(totalConEnvioAtom)
return <Panel tema={tema} productos={productos} total={total} />
}
Fíjate en que las tres fuentes coexisten sin fricción precisamente porque cada una gobierna una clase distinta. productos nunca se copia al store; tema nunca entra en la cache; total nunca se recalcula a mano. El componente es un lector que compone tres capas, no un almacén que las mezcla. Esa limpieza no es casual: es lo que se obtiene cuando la arquitectura respeta las clases de estado en lugar de aplanarlas en un cajón único.
Hay una ventaja silenciosa que solo se aprecia al mantener el código: cada capa se razona y se prueba por separado. Puedes testear la lógica del formulario —los átomos de Jotai— sin montar la red; puedes simular la cache de TanStack Query sin tocar el store de cliente; puedes cambiar el tema en Zustand sin que nada de lo anterior se entere. El acoplamiento que el monolito imponía —todo en un cajón, todo interdependiente— se disuelve cuando cada clase de estado vive en su propia herramienta con su propia frontera.
Sería un error memorizar este nivel como una receta —TanStack Query más Zustand más Jotai— porque las librerías cambiarán y la receta caducará, mientras que el principio del que salen es permanente. Ese principio es el mismo que abrió el track en la ontología: el estado no es una cosa homogénea que un gestor universal deba domar, sino un conjunto de clases con naturalezas distintas —verdad prestada y verdad propia, valor plano y grafo de relaciones, ámbito local y ámbito compartido— y la herramienta correcta para cada clase es la que se ajusta a su naturaleza, no la que promete servir para todas. El stack dominante de 2026 es simplemente lo que ocurre cuando se aplica ese principio con honestidad: aparecen tres herramientas porque hay tres clases dominantes, y aparecerían cuatro o dos si el reparto del estado fuese otro. El monolito de la década anterior encarnaba el principio opuesto —un cajón para todo— y su derrota no fue la de una librería frente a otras, sino la de una idea frente a otra mejor: la idea de que unificar el estado es ordenarlo, cuando en realidad unificarlo es obligar a la clase más simple a pagar el precio de la más difícil y a la más difícil a vivir con las herramientas de la más simple. La madurez que este patrón exige no es saber cablear tres librerías, sino haber interiorizado que la primera pregunta ante cualquier pieza de estado no es dónde la guardo, sino de qué clase es, porque de esa clasificación —y solo de ella— se deduce todo lo demás. Quien clasifica bien elige la herramienta casi sin pensar; quien elige la herramienta primero acaba forzando el estado a caber en ella, y paga ese error en cada dato que vive donde no debe.
- Elige una pantalla real y compleja de tu app. Lista cada pieza de estado que aparece en ella, sin agrupar todavía.
- Clasifica cada pieza en una de las cuatro clases: servidor, cliente global, formulario o derivado, y local o URL. Anota la clase antes de pensar en la herramienta.
- Busca violaciones de frontera: ¿hay algún dato de servidor copiado en tu store? ¿algún formulario global sin motivo? ¿algún total recalculado a mano?
- Migra el estado de servidor a
TanStack Queryy elimina cualquier copia suya que viva en el store. Comprueba que el bug de desincronización desaparece. - Deja en
Zustandsolo lo que el cliente posee y muchos leen; baja auseStateo a la URL lo que no cumpla ese criterio. - Reescribe un cálculo interdependiente como átomos de
Jotaiy verifica que el componente final se limita a leer las tres capas, sin mezclarlas.