Patrón de slices
Receta
Divide stores grandes en slices enfocados y luego combínalos en un solo store. Cada slice define su propio state y acciones de forma independiente.
// stores/slices/user-slice.ts
import { StateCreator } from "zustand";
export interface UserSlice {
user: { name: string; email: string } | null;
setUser: (user: { name: string; email: string }) => void;
clearUser: () => void;
}
export const createUserSlice: StateCreator<UserSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
});// stores/slices/theme-slice.ts
import { StateCreator } from "zustand";
export interface ThemeSlice {
theme: "light" | "dark";
toggleTheme: () => void;
}
export const createThemeSlice: StateCreator<ThemeSlice> = (set) => ({
theme: "light",
toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
});// stores/app-store.ts
import { create } from "zustand";
import { createUserSlice, UserSlice } from "./slices/user-slice";
import { createThemeSlice, ThemeSlice } from "./slices/theme-slice";
type AppStore = UserSlice & ThemeSlice;
export const useAppStore = create<AppStore>()((...args) => ({
...createUserSlice(...args),
...createThemeSlice(...args),
}));Ejemplo funcional
// stores/slices/cart-slice.ts
import { StateCreator } from "zustand";
import type { AuthSlice } from "./auth-slice";
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
export interface CartSlice {
items: CartItem[];
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (id: string) => void;
getCartTotal: () => number;
checkout: () => Promise<void>;
}
// Acceso entre slices mediante el tipo del store combinado
export const createCartSlice: StateCreator<
CartSlice & AuthSlice,
[],
[],
CartSlice
> = (set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
getCartTotal: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
checkout: async () => {
const { items } = get();
const token = get().token; // Accediendo al state de AuthSlice
await fetch("/api/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ items }),
});
set({ items: [] });
},
});// stores/slices/auth-slice.ts
import { StateCreator } from "zustand";
export interface AuthSlice {
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
token: null,
login: async (email, password) => {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const { token } = await res.json();
set({ token });
},
logout: () => set({ token: null }),
});// stores/app-store.ts
import { create } from "zustand";
import { createCartSlice, CartSlice } from "./slices/cart-slice";
import { createAuthSlice, AuthSlice } from "./slices/auth-slice";
type AppStore = CartSlice & AuthSlice;
export const useAppStore = create<AppStore>()((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
}));// components/cart.tsx
"use client";
import { useAppStore } from "@/stores/app-store";
export function Cart() {
const items = useAppStore((s) => s.items);
const removeItem = useAppStore((s) => s.removeItem);
const checkout = useAppStore((s) => s.checkout);
const getCartTotal = useAppStore((s) => s.getCartTotal);
return (
<div>
{items.map((item) => (
<div key={item.id}>
<span>{item.name} x{item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button onClick={() => removeItem(item.id)}>Eliminar</button>
</div>
))}
<p>Total: ${getCartTotal().toFixed(2)}</p>
<button onClick={checkout}>Finalizar compra</button>
</div>
);
}Análisis profundo
Cómo funciona
- Un slice es una función con la firma
StateCreatorque devuelve un objeto de state parcial. - Los slices se combinan expandiéndolos en una única llamada a
create. El store resultante tiene todas las propiedades de todos los slices. - El genérico
StateCreator<CombinedStore, [], [], SliceType>permite que un slice acceda al state de otros slices medianteget(). - Los parámetros
setygeten cada slice hacen referencia al store combinado, no solo al slice. - Los slices se evalúan una vez durante la creación del store. El orden del spread no afecta el comportamiento, ya que todos comparten el mismo
set/get.
Variaciones
Slice con middleware:
import { devtools, persist } from "zustand/middleware";
const useStore = create<AppStore>()(
devtools(
persist(
(...args) => ({
...createCartSlice(...args),
...createAuthSlice(...args),
}),
{ name: "app-store" }
)
)
);Stores independientes en lugar de slices:
// Stores separados para dominios verdaderamente independientes
export const useCartStore = create<CartState>((set) => ({ ... }));
export const useAuthStore = create<AuthState>((set) => ({ ... }));
// Acceso entre stores
const token = useAuthStore.getState().token;Notas de TypeScript
StateCreator<FullStore, Middleware, Middleware, SliceType>es el tipo clave para slices que necesitan acceso entre slices.- Cuando los slices no acceden a otros slices,
StateCreator<SliceType>es suficiente. - El tipo del store combinado es la intersección de todos los tipos de slice.
import { StateCreator } from "zustand";
// Slice simple (sin acceso entre slices)
export const createThemeSlice: StateCreator<ThemeSlice> = (set) => ({ ... });
// Slice con acceso entre slices
export const createCartSlice: StateCreator<
CartSlice & AuthSlice, // Tipo del store completo
[], // Sin mutators (middleware)
[], // Sin mutators
CartSlice // Tipo de este slice
> = (set, get) => ({ ... });Errores comunes
- Los nombres de las propiedades de state deben ser únicos en todos los slices. Si dos slices definen
isLoading, uno sobrescribirá al otro silenciosamente. - El acceso entre slices se tipa mediante el primer genérico de
StateCreator. Si olvidas incluir el tipo de otro slice,get()no tendrá sus propiedades. - El genérico
StateCreatorcon cuatro parámetros de tipo es obligatorio para slices que usan middleware o acceso entre slices. La forma de dos parámetros no funciona. - Dividir demasiado agresivamente en muchos slices minúsculos añade complejidad sin beneficio. Usa slices cuando un store supere las 15-20 propiedades o cuando distintos miembros del equipo trabajen en diferentes funcionalidades.
- El middleware debe envolver el store combinado, no los slices individuales (excepto en casos raros con immer a nivel de slice).
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Patrón de slices | Modular, acceso entre slices posible | Genéricos de TypeScript complejos |
| Stores separados | Verdaderamente independientes, tipos más simples | Sin state compartido, el acceso entre stores requiere getState() |
| Store grande único | No hace falta combinar | Difícil de mantener a escala |
| Slices de Redux Toolkit | Patrón familiar, herramientas integradas | Boilerplate de Redux |
FAQs
¿Qué es un slice en Zustand y por qué usar uno?
- Un slice es una función con la firma
StateCreatorque devuelve un objeto de state parcial. - Usa slices para dividir un store grande en módulos enfocados y mantenidos de forma independiente.
- Combínalos expandiéndolos en una única llamada a
create.
¿Cómo combinas varios slices en un solo store?
type AppStore = UserSlice & ThemeSlice;
export const useAppStore = create<AppStore>()((...args) => ({
...createUserSlice(...args),
...createThemeSlice(...args),
}));¿Cómo accede un slice al state de otro slice?
- Usa la función
get(), que hace referencia al store combinado. - Tipa el slice con
StateCreator<FullStore, [], [], SliceType>para que TypeScript conozca las propiedades del otro slice.
Trampa común: ¿Qué ocurre si dos slices definen una propiedad con el mismo nombre?
- Uno sobrescribe al otro silenciosamente porque los slices se expanden en un único objeto.
- No hay advertencia. Asegúrate de que todos los nombres de propiedades sean únicos entre slices.
Trampa común: ¿Dónde debe aplicarse el middleware: en slices individuales o en el store combinado?
- El middleware debe envolver el store combinado, no los slices individuales.
- Excepción:
immera veces puede usarse a nivel de slice, pero es poco habitual.
const useStore = create<AppStore>()(
devtools(persist((...args) => ({
...createCartSlice(...args),
...createAuthSlice(...args),
}), { name: "app-store" }))
);¿Cuándo deberías usar stores separados en lugar de slices?
- Cuando los dominios son verdaderamente independientes y nunca necesitan compartir state.
- Los stores separados tienen tipos más simples, pero requieren
getState()para el acceso entre stores.
¿Importa el orden al expandir los slices?
- No. Todos los slices comparten el mismo
set/getdel store combinado. - El orden solo importa si hay nombres de propiedades duplicados (gana el último).
¿Cómo tipas un slice que NO accede a otros slices en TypeScript?
import { StateCreator } from "zustand";
const createThemeSlice: StateCreator<ThemeSlice> = (set) => ({
theme: "light",
toggleTheme: () => set((s) => ({
theme: s.theme === "light" ? "dark" : "light",
})),
});¿Cómo tipas un slice que accede a otro slice en TypeScript?
const createCartSlice: StateCreator<
CartSlice & AuthSlice, // Tipo del store completo
[], // Mutators de entrada
[], // Mutators de salida
CartSlice // Tipo de este slice
> = (set, get) => ({
// get().token ahora está tipado desde AuthSlice
});¿Cuál es el tipo del store combinado al usar slices?
- El tipo combinado es la intersección de todos los tipos de slice:
type AppStore = SliceA & SliceB & SliceC. - Se pasa como genérico a
create<AppStore>().
¿En qué momento deberías dividir un store en slices?
- Cuando el store supere las 15-20 propiedades.
- Cuando distintos miembros del equipo trabajen en diferentes funcionalidades dentro del mismo store.
- No dividas demasiado agresivamente: los slices minúsculos añaden complejidad sin beneficio.