React Zustand State Management
Zustand is a lightweight state management library for React. It provides a simple store without the boilerplate of Redux. You can set it up in minutes, and any component can read or update state without wrapping the app in a Provider. It is the fastest-growing state management option in the React ecosystem.
Why Zustand
Redux Toolkit is powerful but structured. Context API is built-in but causes re-render problems at scale. Zustand sits in the middle — simple like Context, performant like Redux, with almost zero setup.
Tool Bundle Size Setup Lines Provider Needed Re-render Opt.
--------------------------------------------------------------------------
Context API 0 KB ~10 YES Manual
Redux Toolkit ~13 KB ~40+ YES Selectors
Zustand ~1 KB ~5 NO Automatic
Installation
npm install zustand
Creating a Store
A Zustand store is a custom hook created with the create function. The store holds state and the functions to update it.
// store/counterStore.js
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 0, // State
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
incrementByAmount: (amount) => set((state) => ({ count: state.count + amount })),
}));
export default useCounterStore;
Using the Store in Components
Call the store hook directly in any component. No Provider, no imports of dispatch or selectors.
import useCounterStore from "./store/counterStore";
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const reset = useCounterStore((state) => state.reset);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Selective Subscriptions Prevent Unnecessary Re-renders
Each component subscribes only to the parts of the store it uses. When other parts change, this component does not re-render.
function CartIcon() {
// Only re-renders when items.length changes
const itemCount = useCartStore((state) => state.items.length);
return <span>Cart ({itemCount})</span>;
}
function CartTotal() {
// Only re-renders when total changes
const total = useCartStore((state) => state.total);
return <p>Total: ${total}</p>;
}
// CartIcon does NOT re-render when total changes
// CartTotal does NOT re-render when item count changes
Real-World Example: Shopping Cart Store
// store/cartStore.js
import { create } from "zustand";
const useCartStore = create((set, get) => ({
items: [],
total: 0,
addItem: (product) => {
const currentItems = get().items;
const existing = currentItems.find(item => item.id === product.id);
if (existing) {
set({
items: currentItems.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: get().total + product.price,
});
} else {
set({
items: [...currentItems, { ...product, quantity: 1 }],
total: get().total + product.price,
});
}
},
removeItem: (productId) => {
const item = get().items.find(i => i.id === productId);
if (!item) return;
set({
items: get().items.filter(i => i.id !== productId),
total: get().total - (item.price * item.quantity),
});
},
clearCart: () => set({ items: [], total: 0 }),
}));
export default useCartStore;
// ProductCard.jsx
function ProductCard({ product }) {
const addItem = useCartStore((state) => state.addItem);
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>Add to Cart</button>
</div>
);
}
Persisting State with localStorage
Zustand has a built-in persist middleware that saves and restores state from localStorage automatically.
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useThemeStore = create(
persist(
(set) => ({
theme: "light",
setTheme: (theme) => set({ theme }),
}),
{
name: "theme-storage", // localStorage key
}
)
);
// State survives page refresh automatically!
function ThemeToggle() {
const { theme, setTheme } = useThemeStore();
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Switch to {theme === "light" ? "Dark" : "Light"} Mode
</button>
);
}
Accessing Store State Outside Components
Zustand lets you read and update the store outside of React components — in utility functions, API handlers, or event listeners.
import useCartStore from "./store/cartStore";
// In an API utility (not a component)
const handleCheckout = async () => {
const { items, total, clearCart } = useCartStore.getState();
await submitOrder({ items, total });
clearCart(); // Directly update store without a component
};
Multiple Stores: Feature-Based Organization
Zustand encourages separate stores per domain rather than one mega-store. This keeps code organized and prevents unrelated state from triggering re-renders.
// Multiple focused stores
import useAuthStore from "./store/authStore";
import useCartStore from "./store/cartStore";
import useNotificationStore from "./store/notificationStore";
function Header() {
const user = useAuthStore((state) => state.user);
const cartCount = useCartStore((state) => state.items.length);
const notifications = useNotificationStore((state) => state.unread);
return (
<header>
<p>Welcome, {user?.name}</p>
<span>Cart: {cartCount}</span>
<span>Alerts: {notifications}</span>
</header>
);
}
Zustand vs Redux Toolkit: Choose Your Tool
Scenario Better choice
--------------------------------------------------------------
Small to medium app with shared state Zustand
Large enterprise app with strict patterns Redux Toolkit
Team already knows Redux Redux Toolkit
You want minimal setup and less code Zustand
Complex async flows with DevTools debugging Redux Toolkit
React Native project Either
You need time-travel debugging Redux Toolkit
Summary
Zustand provides global state management with minimal code and no Provider wrapping required. Create stores with the create function, which takes a callback that receives set and get to define state and updater functions. Components subscribe to specific slices using a selector function, so only the subscribed data causes re-renders. The persist middleware saves state to localStorage with one line of configuration. Call useStore.getState() to access or update the store outside React components. Use multiple focused stores organized by feature to keep state modular and re-renders targeted.
