Zustand is a minimal, fast state manager for React. Here is how to use it effectively in real apps.
Install
pnpm add zustand
Basic Store
// stores/useCounterStore.ts
import { create } from "zustand";
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
Async Actions and Complex State
// stores/useProductStore.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface Filters {
search: string;
category: string | null;
minPrice: number;
maxPrice: number;
}
interface ProductState {
products: Product[];
filters: Filters;
loading: boolean;
error: string | null;
fetchProducts: () => Promise<void>;
setFilter: <K extends keyof Filters>(key: K, value: Filters[K]) => void;
resetFilters: () => void;
filteredProducts: () => Product[];
}
const defaultFilters: Filters = {
search: "",
category: null,
minPrice: 0,
maxPrice: Infinity,
};
export const useProductStore = create<ProductState>()(
devtools(
persist(
immer((set, get) => ({
products: [],
filters: { ...defaultFilters },
loading: false,
error: null,
fetchProducts: async () => {
set({ loading: true, error: null });
try {
const res = await fetch("/api/products");
if (!res.ok) throw new Error("Failed to fetch products");
const products = await res.json();
set({ products, loading: false });
} catch (err) {
set({
error: err instanceof Error ? err.message : "Unknown error",
loading: false,
});
}
},
setFilter: (key, value) => {
set((state) => {
state.filters[key] = value;
});
},
resetFilters: () => {
set({ filters: { ...defaultFilters } });
},
filteredProducts: () => {
const { products, filters } = get();
return products.filter((p) => {
if (filters.search && !p.name.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
if (filters.category && p.category !== filters.category) return false;
if (p.price < filters.minPrice) return false;
if (p.price > filters.maxPrice) return false;
return true;
});
},
})),
{ name: "product-store", partialize: (state) => ({ filters: state.filters }) }
),
{ name: "ProductStore" }
)
);
Slice Pattern for Large Stores
// stores/slices/authSlice.ts
import type { StateCreator } from "zustand";
import type { AppState } from "../useAppStore";
export interface AuthSlice {
user: { id: string; name: string; email: string } | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
export const createAuthSlice: StateCreator<AppState, [], [], AuthSlice> = (
set
) => ({
user: null,
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 }),
});
if (!res.ok) throw new Error("Login failed");
const { user, token } = await res.json();
set({ user, token });
},
logout: () => set({ user: null, token: null }),
});
// stores/slices/uiSlice.ts
export interface UISlice {
sidebarOpen: boolean;
theme: "light" | "dark" | "system";
toggleSidebar: () => void;
setTheme: (theme: UISlice["theme"]) => void;
}
export const createUISlice: StateCreator<AppState, [], [], UISlice> = (
set
) => ({
sidebarOpen: true,
theme: "system",
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme) => set({ theme }),
});
// stores/useAppStore.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { createAuthSlice, type AuthSlice } from "./slices/authSlice";
import { createUISlice, type UISlice } from "./slices/uiSlice";
export type AppState = AuthSlice & UISlice;
export const useAppStore = create<AppState>()(
devtools((...a) => ({
...createAuthSlice(...a),
...createUISlice(...a),
}))
);
Selectors for Performance
// Use selectors to prevent unnecessary re-renders
function UserName() {
const name = useAppStore((s) => s.user?.name);
return <span>{name ?? "Guest"}</span>;
}
// Create reusable selectors
const selectIsAuthenticated = (state: AppState) => state.user !== null;
const selectSidebarOpen = (state: AppState) => state.sidebarOpen;
function Sidebar() {
const isOpen = useAppStore(selectSidebarOpen);
if (!isOpen) return null;
return <aside>...</aside>;
}
// Shallow comparison for object selectors
import { useShallow } from "zustand/react/shallow";
function UserCard() {
const { name, email } = useAppStore(
useShallow((s) => ({
name: s.user?.name ?? "",
email: s.user?.email ?? "",
}))
);
return (
<div>
<p>{name}</p>
<p>{email}</p>
</div>
);
}
Subscribe Outside React
// Listen for state changes outside components
const unsubscribe = useAppStore.subscribe(
(state) => state.user,
(user, prevUser) => {
if (user && !prevUser) {
console.log("User logged in:", user.email);
}
if (!user && prevUser) {
console.log("User logged out");
}
}
);
Need Help Building Complex React Apps?
We design scalable frontend architectures for production applications. Get in touch to discuss your project.