//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Split large stores into focused slices, then combine them into a single store. Each slice defines its own state and actions independently.
// 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),
}));// 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>;
}
// Cross-slice access via the combined store type
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; // Accessing AuthSlice state
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)}>Remove</button>
</div>
))}
<p>Total: ${getCartTotal().toFixed(2)}</p>
<button onClick={checkout}>Checkout</button>
</div>
);
}StateCreator signature that returns a partial state object.create call. The resulting store has all properties from all slices.StateCreator<CombinedStore, [], [], SliceType> generic allows a slice to access state from other slices via get().set and get parameters in each slice refer to the combined store, not just the slice.set/get.Slice with middleware:
import { devtools, persist } from "zustand/middleware";
const useStore = create<AppStore>()(
devtools(
persist(
(...args) => ({
...createCartSlice(...args),
...createAuthSlice(...args),
}),
{ name: "app-store" }
)
)
);Independent stores instead of slices:
// Separate stores for truly independent domains
export const useCartStore = create<CartState>((set) => ({ ... }));
export const useAuthStore = create<AuthState>((set) => ({ ... }));
// Cross-store access
const token = useAuthStore.getState().token;StateCreator<FullStore, Middleware, Middleware, SliceType> is the key type for slices that need cross-slice access.StateCreator<SliceType> is sufficient.import { StateCreator } from "zustand";
// Simple slice (no cross-access)
export const createThemeSlice: StateCreator<ThemeSlice> = (set) => ({ ... });
// Cross-access slice
export const createCartSlice: StateCreator<
CartSlice & AuthSlice, // Full store type
[], // No mutators (middleware)
[], // No mutators
CartSlice // This slice's type
> = (set, get) => ({ ... });isLoading, one will overwrite the other silently.StateCreator. Forgetting to include another slice's type means get() will not have its properties.StateCreator generic with four type parameters is required for slices that use middleware or cross-access. The two-parameter form does not work.| Approach | Pros | Cons |
|---|---|---|
| Slice pattern | Modular, cross-slice access possible | Complex TypeScript generics |
| Separate stores | Truly independent, simpler types | No shared state, cross-store access requires getState() |
| Single large store | No combining needed | Hard to maintain at scale |
| Redux Toolkit slices | Familiar pattern, built-in tooling | Redux boilerplate |
StateCreator signature that returns a partial state object.create call.type AppStore = UserSlice & ThemeSlice;
export const useAppStore = create<AppStore>()((...args) => ({
...createUserSlice(...args),
...createThemeSlice(...args),
}));get() function, which references the combined store.StateCreator<FullStore, [], [], SliceType> so TypeScript knows about the other slice's properties.immer can sometimes be used at the slice level, but this is rare.const useStore = create<AppStore>()(
devtools(persist((...args) => ({
...createCartSlice(...args),
...createAuthSlice(...args),
}), { name: "app-store" }))
);getState() for cross-store access.set/get from the combined store.import { StateCreator } from "zustand";
const createThemeSlice: StateCreator<ThemeSlice> = (set) => ({
theme: "light",
toggleTheme: () => set((s) => ({
theme: s.theme === "light" ? "dark" : "light",
})),
});const createCartSlice: StateCreator<
CartSlice & AuthSlice, // Full store type
[], // Mutators in
[], // Mutators out
CartSlice // This slice's type
> = (set, get) => ({
// get().token is now typed from AuthSlice
});type AppStore = SliceA & SliceB & SliceC.create<AppStore>().Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥