Guía de gestión de estado en Next.js: Zustand vs Jotai en la práctica

El mensaje de error volvió a aparecer: la decimoséptima vez que modifico el action creator de Redux. El proyecto solo tiene un carrito de compra y ya hay tres archivos de configuración.
¿Por qué tiene que ser tan complicado? El proyecto no es grande; Redux parece matar moscas a cañonazos. Probé Context API y, con una sola actualización de estado, media página se re-renderizó. El panel de rendimiento en rojo da dolor de cabeza.
Por eso Zustand y Jotai han ganado tanto terreno en los últimos años: prometen ligereza y alto rendimiento. Pero, ¿cuál elegir? En este artículo hablamos de:
- Por qué Redux y Context no encajan (trampas reales)
- En qué se diferencian Zustand y Jotai en esencia
- Qué escenario conviene a cada uno (árbol de decisión)
- Cómo usarlos en Next.js App Router (experiencia con los errores típicos)
¿Por qué no Redux ni Context?
¿En qué es «pesado» Redux?
Empecemos por Redux. No es malo; para muchos proyectos simplemente es demasiado.
Hay que escribir action types, action creators, reducers y configurar el store. Una acción simple como «añadir al carrito» puede tocar tres o cuatro archivos. Ese boilerplate cansa.
Además, si hay gente nueva en el equipo, la curva de aprendizaje es empinada. ¿Qué es dispatch? ¿Por qué pure functions? ¿Para qué sirve el middleware? Hay que dedicar tiempo.
Para una lista de tareas o un blog personal, Redux es como ir en tanque a comprar el pan: llegas, pero no hace falta.
La trampa de rendimiento de Context API
Context es simple: viene con React y no requiere librerías.
Pero tiene un problema grande: el rendimiento.
Cuando cambia el value del Provider, todos los componentes que consumen ese Context se re-renderizan. Aunque solo uses un campo, el componente entero vuelve a renderizarse.
Hice un formulario con Context para los campos. Un onChange en un input re-renderizó 20 componentes en la página. El flame chart de Chrome DevTools dolía de ver.
Puedes optimizar con memo, useMemo o dividiendo Context, pero el código optimizado ya no es más simple que Redux.
El atractivo de las soluciones ligeras
Ahí entran Zustand y Jotai.
Prometen cosas claras:
- API simple, rápida de aprender (Zustand en unos 10 minutos)
- Optimización de rendimiento integrada
- Paquetes pequeños (Zustand ~1 KB, aún menos con gzip)
Los datos lo confirman: en 2025, el uso de Zustand creció un 150% en un año. Cada vez más equipos dejan Redux por opciones ligeras.
Y surge la pregunta: ¿Zustand o Jotai?
Diferencias clave: Zustand vs Jotai
A simple vista ambas son «gestión de estado ligera», pero el diseño interno es distinto.
Modelo de estado: tienda única vs puestos atómicos
La documentación lo resume bien: «Zustand is like Redux. Jotai is like Recoil.»
Zustand es un Redux simplificado: un store con todo el estado, como un gran centro comercial.
// Zustand: un gran store
const useStore = create((set) => ({
user: null,
cart: [],
theme: 'light',
// todo el estado aquí
}))
Jotai es atómico: cada estado es un atom independiente, como puestos separados.
// Jotai: atoms independientes
const userAtom = atom(null)
const cartAtom = atom([])
const themeAtom = atom('light')
Esa diferencia marca los escenarios donde encaja cada uno.
Dónde vive el estado: fuera del árbol vs dentro
El store de Zustand es a nivel de módulo, fuera de React. Puedes importarlo y actualizarlo en cualquier sitio, sin Provider.
Los atoms de Jotai viven en el árbol de componentes y dependen de Context. Necesitas un Provider en la raíz para compartirlos.
¿Qué implica?
Si debes actualizar estado fuera de componentes React (utilidades, callbacks de WebSocket), Zustand es más cómodo. Jotai también puede, pero con más rodeos.
Rendimiento: optimización manual vs automática
La estrategia difiere.
La suscripción atómica de Jotai suele ser óptima por defecto: el componente solo escucha los atoms que usa; otros no provocan re-render.
En Zustand conviene usar selectores:
// No recomendado: suscribirse a todo el store
const store = useStore()
// Recomendado: selector solo de lo necesario
const user = useStore(state => state.user)
Los selectores de Zustand son sencillos e intuitivos; solo hay que acordarse de usarlos.
Resumen en una frase
- Zustand: store único, fuera de React, selectores manuales
- Jotai: atoms, dentro de React, rendimiento optimizado por defecto
La elección depende de tu proyecto.
¿Cuándo elegir Zustand?
Si tu proyecto encaja aquí, Zustand suele ser la mejor opción.
Apps pequeñas y medianas sin complicarse
La mayoría de proyectos no necesitan gestión de estado compleja.
En un e-commerce global suele bastar: usuario, carrito, tema. Zustand encaja perfecto.
La API es tan simple que casi no hay curva de aprendizaje. Ejemplo completo:
// store.js
import create from 'zustand'
const useStore = create((set) => ({
cart: [],
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
removeFromCart: (id) => set((state) => ({
cart: state.cart.filter(item => item.id !== id)
})),
}))
// CartButton.jsx
function CartButton() {
const addToCart = useStore(state => state.addToCart)
return <button onClick={() => addToCart(item)}>Añadir al carrito</button>
}
// CartCount.jsx
function CartCount() {
const count = useStore(state => state.cart.length)
return <span>{count}</span>
}
Sin Provider, sin action types, sin reducer: defines estado y métodos y listo.
Un compañero nuevo lo entiende en 10 minutos.
Actualizar estado fuera de React
Es una ventaja distintiva de Zustand.
Por ejemplo, un WebSocket que actualiza estado al recibir mensajes:
// websocket.js
import { useStore } from './store'
socket.on('message', (data) => {
// Llamar al store directamente, sin estar en un componente
useStore.getState().updateMessages(data)
})
O en una utilidad que consulta el estado actual:
// utils.js
import { useStore } from './store'
export function checkPermission() {
const user = useStore.getState().user
return user?.role === 'admin'
}
En Jotai los atoms están ligados al árbol; acceder desde fuera es menos natural.
Compatible con SSR en Next.js
Zustand está muy maduro en Next.js.
La documentación oficial tiene un capítulo de integración y buenas prácticas con App Router. La comunidad ya documentó la mayoría de trampas.
Con Next.js 13+ App Router, Zustand es de las opciones más estables. Más abajo detallo la configuración.
¿Cuándo no conviene Zustand?
Cuando hay dependencias derivadas complejas entre estados.
Un filtro con 10 criterios donde las opciones de cada uno dependen de los demás: con Zustand el código se enreda.
Ahí brilla el diseño atómico de Jotai.
¿Cuándo elegir Jotai?
El modelo atómico brilla en ciertos escenarios.
Dependencias de estado complejas
Es el punto fuerte de Jotai.
Imagina un filtro de productos:
- Criterios: marca, rango de precio, valoración
- Las marcas disponibles dependen del rango de precio
- La lista final depende de todos los filtros
Con Zustand gestionas dependencias a mano y el código se desordena.
Con Jotai queda claro:
// Atoms base
const brandAtom = atom([])
const priceRangeAtom = atom([0, 1000])
const ratingAtom = atom(0)
// Atom derivado: marcas disponibles (depende del precio)
const availableBrandsAtom = atom((get) => {
const priceRange = get(priceRangeAtom)
return fetchBrands(priceRange) // reacciona al cambio de priceRange
})
// Atom derivado: productos filtrados (depende de todo)
const filteredProductsAtom = atom((get) => {
const brands = get(brandAtom)
const priceRange = get(priceRangeAtom)
const rating = get(ratingAtom)
return products.filter(/* lógica de filtro */)
})
Cada atom solo mira sus dependencias; Jotai las rastrea y actualiza lo necesario.
En componentes:
function FilterPanel() {
const [brands, setBrands] = useAtom(brandAtom)
const availableBrands = useAtomValue(availableBrandsAtom)
// Si brands cambia, availableBrands se recalcula solo
}
En este escenario Jotai es mucho más legible que Zustand.
Rendimiento extremo
La suscripción atómica de Jotai es muy buena.
Un dashboard en tiempo real con 50 componentes y métricas distintas: con Context o Zustand sin optimizar, una actualización re-renderiza muchos nodos.
Con Jotai cada componente solo escucha su atom.
La documentación lo dice: «This is the most performant by default.» Solo re-renderiza quien suscribe el atom que cambió.
Apps grandes con code splitting
Los atoms se pueden cargar bajo demanda.
Puedes repartirlos en archivos e importar solo cuando hace falta, ayudando al primer paint en apps grandes.
El store de Zustand suele ser un bloque; también se puede dividir, pero menos natural que Jotai.
Proyectos que usan Suspense a fondo
Si usas mucho React Suspense (datos asíncronos), Jotai tiene soporte nativo.
Un atom asíncrono es directo:
const userAtom = atom(async () => {
const res = await fetch('/api/user')
return res.json()
})
function UserProfile() {
const user = useAtomValue(userAtom)
// Suspense automático hasta tener datos
return <div>{user.name}</div>
}
Zustand puede combinarse con Suspense, pero requiere más envoltorio.
Curva de aprendizaje de Jotai
Jotai es un poco más abstracto que Zustand.
Lectura/escritura de atoms, atoms derivados y asíncronos requieren tiempo. En equipos nuevos el onboarding es más lento.
Su documentación, siendo honestos, es menos amigable que la de Zustand; a veces hay que leer varias veces.
Buenas prácticas con Next.js App Router
Pasemos a lo práctico. Con Next.js 13+ App Router hay trampas que conviene conocer antes.
Zustand en Next.js: la forma correcta
Trampa grande: no uses un store global
Muchos (yo incluido al principio) escriben así:
// ❌ Incorrecto: store global
import create from 'zustand'
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}))
En CSR puede funcionar, pero en SSR de Next.js el store se comparte entre peticiones. Los datos del usuario A pueden verse en la sesión del B: riesgo de seguridad.
La recomendación oficial es el patrón Store Factory:
// lib/store.js
import { createStore } from 'zustand/vanilla'
export function createUserStore(initialState) {
return createStore((set) => ({
user: initialState?.user || null,
setUser: (user) => set({ user })
}))
}
Luego un Provider en componente cliente:
// components/StoreProvider.jsx
'use client'
import { createContext, useContext, useRef } from 'react'
import { useStore } from 'zustand'
import { createUserStore } from '@/lib/store'
const StoreContext = createContext(null)
export function StoreProvider({ children, initialState }) {
const storeRef = useRef()
if (!storeRef.current) {
storeRef.current = createUserStore(initialState)
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
}
export function useUserStore(selector) {
const store = useContext(StoreContext)
return useStore(store, selector)
}
En el layout raíz:
// app/layout.jsx
import { StoreProvider } from '@/components/StoreProvider'
export default function RootLayout({ children }) {
// Aquí puedes precargar datos
const initialState = { user: null }
return (
<html>
<body>
<StoreProvider initialState={initialState}>
{children}
</StoreProvider>
</body>
</html>
)
}
Así cada petición tiene su store y no se mezclan datos.
Notas:
- Los Server Components no leen ni escriben el store directamente
- El prefetch va en el servidor y llega al cliente vía
initialState - No bloquees el layout raíz con fetch; perjudica el rendimiento
Hydration SSR con Jotai
La trampa principal de Jotai en Next.js son los errores de hydration.
Provider independiente por petición:
// app/providers.jsx
'use client'
import { Provider } from 'jotai'
export function Providers({ children }) {
return <Provider>{children}</Provider>
}
// app/layout.jsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
Datos del servidor:
Para inyectar datos del servidor en atoms, usa useHydrateAtoms:
'use client'
import { useHydrateAtoms } from 'jotai/utils'
import { userAtom } from '@/atoms'
export function HydrateAtoms({ initialUser, children }) {
useHydrateAtoms([[userAtom, initialUser]])
return children
}
Trampa: useHydrateAtoms solo actúa en el primer render. Con router.push en App Router, la segunda visita al mismo atom no se vuelve a hidratar.
Soluciones:
- Poner el Provider en
template.tsxen lugar delayout.tsx(se recrea en cada ruta) - O un Provider a nivel de página, no global
Errores con atomWithStorage:
Con atomWithStorage para formularios puedes tener mismatch de hydration:
- Servidor: formulario vacío
- Cliente: valor desde
localStorage - Resultado: error de hydration mismatch en React
Solución: rellenar en useEffect, o useHydrateAtoms + useSyncExternalStore.
Principios comunes (ambas librerías)
-
Los Server Components no usan gestión de estado
- No tienen hooks; no puedes usar
useStoreniuseAtom - Marca
'use client'donde haga falta estado
- No tienen hooks; no puedes usar
-
La posición del Provider importa
- Cuanto más profundo, mejor para optimizar partes estáticas
- Pero debe ser accesible para quien necesite estado
-
Evita bloquear el layout raíz
- No hagas
await fetch()de datos de usuario en root layout - Anula streaming y las ventajas de Server Components
- Usa componentes cliente dedicados para fetch e inicialización
- No hagas
Estas trampas las he pisado yo. Con estos principios ahorras mucho tiempo de depuración.
Mi recomendación de elección
Tras todo esto, la pregunta sigue siendo: ¿cuál elijo?
No hay respuesta única, pero sí un árbol de decisión.
Árbol rápido
Si tu proyecto es…
-
Proyecto personal / app pequeña
→ Empieza con Context API; si basta, no cambies
→ Si hay problemas de rendimiento, pasa a Zustand -
SaaS mediano / e-commerce
→ Zustand directamente
→ Simple, estable, fácil para el equipo -
Dashboard complejo / app en tiempo real
→ Valora Jotai
→ Dependencias complejas, código más claro -
Equipo grande / normas estrictas
→ Redux Toolkit puede encajar mejor
→ Más convenciones y buenas prácticas documentadas -
Actualizar estado fuera de React
→ Zustand
→ Jotai puede, pero menos natural -
Uso intensivo de Suspense
→ Jotai fluye mejor
→ Atoms asíncronos nativos
Estrategia progresiva
Recomiendo ir por fases:
-
Fase 1: Context API
- Al inicio, poco estado; Context basta
- No cambies si funciona; evita optimizar antes de tiempo
-
Fase 2: Zustand
- Context empieza a doler en rendimiento
- O el estado global se complica
- Zustand cubre ~80% de casos
-
Fase 3: Jotai o quedarse en Zustand
- Dependencias muy complejas → Jotai
- Si no, quédate en Zustand
- No elijas tecnología por moda
¿Se pueden mezclar?
¡Sí!
Zustand y Jotai no se excluyen. He visto proyectos con:
- Zustand para configuración global (usuario, tema)
- Jotai para formularios complejos
Sin problema. La herramienta adecuada para cada problema.
Mi experiencia
- Blog personal: sin gestión de estado; Server Components + estado en URL bastan
- Panel de administración: Zustand + React Query para estado del servidor
- Dashboard en tiempo real: Jotai; las dependencias eran demasiado enredadas con Zustand
No obsesionarse con la elección al día uno. Empieza simple y sube de nivel cuando duela.
Muchos proyectos viven bien con Context API. No la subestimes.
Conclusión
Volviendo al inicio.
¿Redux es pesado? Sí, para muchos proyectos es exceso.
¿Context rinde mal? Sí, sin optimizar el re-render se convierte en cuello de botella.
¿Zustand o Jotai? Depende:
- En la mayoría de casos, Zustand basta: simple y estable
- Con dependencias complejas, Jotai es más elegante
- ¿Dudas? Prueba Zustand y cambia si hace falta
¿App Router? Tres ideas:
- No uses store global
- Provider independiente por petición
- Los Server Components no tocan estado
Una última cosa.
No hay respuesta estándar en arquitectura. No te dejes llevar solo por comparativas en internet, incluida esta. Lo importante es una solución cómoda para ti y tu equipo que resuelva el problema real.
Si dudas ahora: elige una, haz un demo y prueba. Diez minutos de práctica valen más que diez artículos.
Que encuentres la herramienta que te encaje.
FAQ
¿Cuál es la diferencia entre Redux, Context, Zustand y Jotai?
• Ventajas: potente, ecosistema amplio, ideal para proyectos muy grandes
• Desventajas: muy pesado, mucho código repetitivo, curva de aprendizaje alta
• Uso: proyectos muy grandes con gestión de estado compleja
Context API:
• Ventajas: integrado en React, sin librerías extra
• Desventajas: mal rendimiento; una actualización puede re-renderizar todo el subárbol
• Uso: estado global simple que no se actualiza con frecuencia
Zustand:
• Ventajas: simple y directo, poco código, fácil de aprender
• Desventajas: no ideal para proyectos enormes
• Uso: la mayoría de proyectos, pequeños y medianos
Jotai:
• Ventajas: estado atómico, actualizaciones granulares, buen rendimiento
• Desventajas: curva de aprendizaje más alta, código más complejo
• Uso: proyectos grandes que exigen rendimiento máximo
Recomendación: Zustand para la mayoría; Jotai para proyectos grandes o cuando el rendimiento es crítico.
¿Cuándo usar Zustand y cuándo Jotai?
• La mayoría de proyectos
• Proyectos pequeños y medianos
• Necesitas gestión de estado simple y directa
• Bajo coste de aprendizaje para el equipo
• Poco código
Escenarios para Jotai:
• Proyectos grandes
• Rendimiento extremo
• Actualizaciones de estado frecuentes
• Actualizaciones granulares
• Equipo con experiencia
Árbol de decisión:
• Proyecto pequeño → Zustand
• Proyecto grande → Jotai
• Alto requisito de rendimiento → Jotai
• Simplicidad → Zustand
Recomendación: Zustand para la mayoría; Jotai solo en proyectos grandes o con requisitos de rendimiento extremos.
¿Por qué Context API tiene mal rendimiento?
Soluciones:
• Dividir Context (separar por funcionalidad)
• Optimizar con useMemo y memo
• Cambiar a Zustand o Jotai (soportan suscripción granular)
Recomendación: si el estado se actualiza con frecuencia, no uses Context API; mejor Zustand o Jotai.
¿Cómo usar Zustand en Next.js App Router?
1. Usar createStore para crear una función fábrica de store
2. En componentes cliente, crear la instancia del store con useRef
3. Pasar la instancia a los hijos mediante un Context Provider
Puntos clave:
• Los componentes que usan el store deben llevar 'use client'
• Los Server Components no pueden usar gestión de estado
• Cada petición debe tener su propia instancia de store para evitar mezcla de datos
Así cada petición tiene estado independiente y se mantiene la simplicidad de Zustand.
¿Cómo usar Jotai en Next.js App Router?
1. Envolver con el componente Provider en el layout raíz o en template.jsx
2. Usar useHydrateAtoms para inyectar datos del servidor como valor inicial
3. Nota: useHydrateAtoms solo actúa en el primer render
Puntos clave:
• Los componentes que usan atoms deben ser Client Components
• Los Server Components no pueden usar gestión de estado
• Diseño atómico con actualizaciones granulares automáticas
Ventajas:
• Solo se actualizan los componentes que suscriben un atom concreto
• Soporte nativo de Suspense y datos asíncronos
• Ideal para dependencias complejas y proyectos grandes
Nota: Jotai cuesta más aprender que Zustand, pero en escenarios complejos el rendimiento y la claridad del código compensan.
¿Pueden los Server Components usar gestión de estado?
Enfoque correcto:
• Los Server Components obtienen datos (fetch, consultas a BD, etc.)
• Pasan los datos a Client Components mediante props
• Los Client Components gestionan estado e interacción
Patrón:
Server Component obtiene datos iniciales → props → Client Component con Zustand/Jotai para estado e interacción
Esta división deja al servidor el fetch y el SEO, y al cliente la interacción y el estado, aprovechando App Router.
13 min de lectura · Publicado el: 19 dic 2025 · Actualizado el: 21 ago 2026
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía completa de Next.js + Prisma: de la configuración a la práctica (con solución a la fuga de conexiones)
Tutorial completo de Next.js + Prisma: configuración del entorno, diseño de Schema, CRUD en la práctica y solución a la fuga de conexiones por hot reload, para que empieces rápido con Prisma ORM.
Parte 23 de 51
Siguiente
Guía completa del mecanismo de caché en Next.js: cuándo usar revalidate correctamente
Análisis profundo de las cuatro capas de caché en Next.js, cuándo usar revalidate, revalidatePath y revalidateTag, solución a problemas habituales como datos que no se actualizan, con guía completa de diagnóstico y mejores prácticas
Parte 25 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario