Zustand
Almacenes globales ligeros con mínima boilerplate. Zustand se ajusta al estado de la UI del cliente que persiste a través de cambios de pantalla - carrito, preferencias, finalización del recorrido de características. Combina con TanStack Query para datos de API; no dupliques cachés del servidor en Zustand.
Tarjeta de referencia rápida - lista para copiar-pegar.
// src/features/cart/cartStore.ts
import { create } from "zustand" ;
type CartItem = { productId : string ; qty : number };
type CartState = {
items : CartItem [];
add : ( item : CartItem ) => void ;
remove : ( productId : string ) => void ;
totalQty : () => number ;
};
export const useCartStore = create < CartState >(( set , get ) => ({
items: [],
add : ( item ) =>
set (( s ) => {
const existing = s.items. find (( i ) => i.productId === item.productId);
if (existing) {
return {
items: s.items. map (( i ) =>
i.productId === item.productId ? { ... i, qty: i.qty + item.qty } : i
),
};
}
return { items: [ ... s.items, item] };
}),
remove : ( productId ) => set (( s ) => ({ items: s.items. filter (( i ) => i.productId !== productId) })),
totalQty : () => get ().items. reduce (( n , i ) => n + i.qty, 0 ),
}));
// Consumidor - el selector limita re-renderizados
import { useCartStore } from "@/features/cart/cartStore" ;
const qty = useCartStore (( s ) => s. totalQty ());
Cuándo usarlo:
Múltiples pantallas leen/escriben el mismo estado del cliente (insignia del carrito, filtros persistidos localmente).
Context causa re-renderizados visibles en el profiler - los selectores lo arreglan.
Quieres un bundle pequeño (~1KB) sin la ceremonia de Redux.
Las actualizaciones de estado son UI síncrona - no caché del servidor con tiempos de expiración.
Valor por defecto del equipo desde ADR para UI del cliente global.
// stores/preferencesStore.ts - patrón slice + devtools en desarrollo
import { create } from "zustand" ;
import { devtools } from "zustand/middleware" ;
type ThemeMode = "system" | "light" | "dark" ;
type PreferencesState = {
theme : ThemeMode ;
hapticsEnabled : boolean ;
setTheme : ( theme : ThemeMode ) => void ;
toggleHaptics : () => void ;
};
export const usePreferencesStore = create < PreferencesState >()(
devtools (
( set ) => ({
theme: "system" ,
hapticsEnabled: true ,
setTheme : ( theme ) => set ({ theme }, false , "preferences/setTheme" ),
toggleHaptics : () => set (( s ) => ({ hapticsEnabled: ! s.hapticsEnabled }), false , "preferences/toggleHaptics" ),
}),
{ name: "PreferencesStore" , enabled: __DEV__ }
)
);
// stores/cartStore.ts - almacén de dominio separado
import { create } from "zustand" ;
type CartItem = { productId : string ; title : string ; qty : number };
type CartState = {
items : CartItem [];
add : ( item : CartItem ) => void ;
clear : () => void ;
};
export const useCartStore = create < CartState >(( set ) => ({
items: [],
add : ( item ) => set (( s ) => ({ items: [ ... s.items, item] })),
clear : () => set ({ items: [] }),
}));
// --- UI ---
import * as Haptics from "expo-haptics" ;
import { Pressable, Text, View } from "react-native" ;
export function SettingsScreen () {
const theme = usePreferencesStore (( s ) => s.theme);
const hapticsEnabled = usePreferencesStore (( s ) => s.hapticsEnabled);
const setTheme = usePreferencesStore (( s ) => s.setTheme);
const toggleHaptics = usePreferencesStore (( s ) => s.toggleHaptics);
function onToggleHaptics () {
if (hapticsEnabled) void Haptics. selectionAsync ();
toggleHaptics ();
}
return (
< View style = {{ padding: 16 , gap: 12 }}>
< Text >Theme: {theme}</ Text >
< Pressable onPress = {() => setTheme (theme === "dark" ? "light" : "dark" )}>
< Text >Toggle theme</ Text >
</ Pressable >
< Pressable onPress = {onToggleHaptics}>
< Text >Haptics: {hapticsEnabled ? "On" : "Off" }</ Text >
</ Pressable >
</ View >
);
}
export function CartBadge () {
const count = useCartStore (( s ) => s.items. reduce (( n , i ) => n + i.qty, 0 ));
return < Text >Cart ({count})</ Text >;
}
// cartStore.test.ts
import { useCartStore } from "./cartStore" ;
const initial = useCartStore. getState ();
beforeEach (() => {
useCartStore. setState (initial, true );
});
test ( "add merges quantity" , () => {
useCartStore. getState (). add ({ productId: "1" , title: "Pack" , qty: 1 });
useCartStore. getState (). add ({ productId: "1" , title: "Pack" , qty: 2 });
expect (useCartStore. getState ().items). toHaveLength ( 2 ); // o política de fusión por reglas de producto
});
Lo que esto demuestra:
Almacenes separados por dominio - los cambios de preferencias no afectan a los suscriptores del carrito.
El middleware devtools nombra acciones para Redux DevTools solo en desarrollo.
Los selectores en useCartStore mantienen actualizaciones de insignia de pestaña baratas.
setState(..., true) reemplaza todo el estado en pruebas - reinicio determinístico.
Haptics permanecen en el componente - el almacén contiene banderas, no efectos secundarios nativos.
Combina slices relacionados cuando comparten el espacio de nombres de devtools:
import { create } from "zustand" ;
type BearSlice = { bears : number ; addBear : () => void };
type FishSlice = { fish : number ; addFish : () => void };
type Store = BearSlice & FishSlice ;
export const useBoundStore = create < Store >()(( ... a ) => ({
... createBearSlice ( ... a),
... createFishSlice ( ... a),
}));
function createBearSlice ( set : Parameters < typeof create< BearSlice >>[ 0 ]) : BearSlice {
return {
bears: 0 ,
addBear : () => set (( s ) => ({ bears: s.bears + 1 })),
};
}
Prefiere múltiples almacenes create() hasta que se requieran transacciones entre slices.
import AsyncStorage from "@react-native-async-storage/async-storage" ;
import { create } from "zustand" ;
import { createJSONStorage, persist } from "zustand/middleware" ;
export const usePreferencesStore = create (
persist < PreferencesState >(
( set ) => ({
theme: "system" ,
hapticsEnabled: true ,
setTheme : ( theme ) => set ({ theme }),
toggleHaptics : () => set (( s ) => ({ hapticsEnabled: ! s.hapticsEnabled })),
}),
{
name: "preferences-v1" ,
storage: createJSONStorage (() => AsyncStorage),
partialize : ( s ) => ({ theme: s.theme, hapticsEnabled: s.hapticsEnabled }),
}
)
);
partialize excluye acciones del disco - solo serializa campos de datos.
Nunca persistas tokens - usa expo-secure-store a través de un adaptador de almacenamiento personalizado - consulta State Persistence & Hydration .
Aumenta name (preferences-v2) cuando el esquema cambia - o ejecuta migración en onRehydrateStorage.
El middleware devtools envía acciones a la extensión Redux DevTools al depurar JS a través de Flipper o escritorio remoto. Deshabilita en producción: enabled: __DEV__.
import { subscribeWithSelector } from "zustand/middleware" ;
useCartStore. subscribe (
( s ) => s.items. length ,
( len ) => console. log ( "cart size" , len)
);
Usa para efectos secundarios de análisis - no para renderizaciones de React.
import { immer } from "zustand/middleware/immer" ;
create < CartState >()(
immer (( set ) => ({
items: [],
add : ( item ) =>
set (( state ) => {
state.items. push (item);
}),
}))
);
Seleccionar todo el almacén - useCartStore() sin selector se re-renderiza en cada cambio de carrito. Solución: useCartStore((s) => s.items.length).
Datos derivados en almacén sin memoización - get().items.map(...) en renderizado a través de selector se ejecuta cada vez. Solución: Selector calculado o useMemo en el resultado.
Almacenar respuestas de API - La invalidación del caché se vuelve manual. Solución: TanStack Query.
Efectos secundarios dentro de set - Llamar a fetch en add() acopla el almacén a la red. Solución: El componente o thunk llama a la API, luego set.
Un almacén gigante - Ciclos de importación y dolor de recarga en caliente. Solución: features/cart/cartStore.ts, features/settings/preferencesStore.ts.
Persistir todo - JSON de carrito grande ralentiza el inicio en frío. Solución: partialize, TTL, o persistencia de Query para lecturas del servidor.
Olvidar reinicio de prueba - Las pruebas filtran estado dependiente del orden. Solución: beforeEach(() => useCartStore.setState(initial, true)).
Alternativa Usar cuando No usar cuando Zustand UI del cliente global + selectores Caché de lista del servidor Context (split) Tema/sesión, cero deps Actualizaciones de carrito de alta frecuencia Redux Toolkit Auditoría + ecosistema de middleware Greenfield solo con carrito + tema Jotai Gráficos de UI derivados atómicos Equipo no familiarizado con átomos MMKV raw Persistencia crítica de velocidad Aún necesitas semántica de actualización
¿Necesita Zustand un Provider?
No - los almacenes son singletons de módulo. Múltiples raíces de React (raras en RN) compartirían el mismo almacén a menos que pases createStore a través de context.
¿Zustand vs Redux Toolkit en 2026?
Zustand por defecto para estado global del cliente. Redux cuando DevTools, middleware y estándares organizacionales requieren reductores centralizados - consulta Redux Toolkit .
¿Cómo uso Zustand con Expo Router?
Importa hooks en cualquier archivo de ruta. Orquesta navegación en la ruta; el almacén contiene datos. Evita importar router dentro de módulos de almacén.
¿Puede Zustand reemplazar TanStack Query?
No para estado del servidor. Zustand puede contener banderas de UI optimista; Query es propietario de entidades obtenidas y claves de caché.
¿Cuál es el impacto del bundle?
Zustand es ~1-3KB comprimido - negligible vs un único recurso de imagen. Redux Toolkit es más grande pero incluye RTK Query.
¿Patrón de acciones asincrónicas?
Mantén async en hooks o servicios:
async function checkout () {
set ({ status: "pending" });
try {
await api. checkout ( get ().items);
set ({ status: "done" , items: [] });
} catch (e) {
set ({ status: "error" });
}
}
¿Mejor práctica de TypeScript?
Define el tipo CartState explícitamente; usa create<CartState>()(...) con middleware para tipos de middleware inferidos.
¿Cómo rehidrato al iniciar la aplicación?
Usa persist + callback onRehydrateStorage para controlar la UI - consulta State Persistence & Hydration .
¿Múltiples componentes mismo selector?
Zustand usa Object.is en el resultado del selector - devuelve primitivas o referencias estables para evitar renderizados adicionales.
¿Integración con React Compiler?
Los selectores siguen siendo recomendados - la memoización del compilador no reemplaza suscripciones de almacén entre componentes.
¿Limpieza de cierre de sesión?
useCartStore.setState({ items: [] }) más queryClient.clear() - cachés del cliente y del servidor deben limpiarse juntos.
¿Puedo usar Zustand con immer y devtools juntos?
Sí - el orden del middleware importa: devtools(persist(immer(...))) - consulta la documentación de Zustand para tu cadena de middleware.
Versiones de stack: Esta página fue escrita para React 19.2.3 , React Native 0.86.0 y Expo SDK 57 (expo ~57.0.4).