Zustand: Minimal Global State Management for React
useState works fine while a piece of state belongs to one component. As soon as several unrelated components need to read and update the same value — a logged-in user, a shopping cart, a theme toggle — you need somewhere shared to put it. Zustand is a small library that gives you that shared place without the ceremony Redux is known for.
(Note: this is about client state — UI flags, form values, app-wide preferences. If what you’re managing is actually data from a server, TanStack Query is usually the better tool; the two solve different problems and often get used side by side.)
The Problem: Sharing State Without the Boilerplate
React’s built-in answer to shared state is Context: wrap a subtree in a Provider, read the value with useContext. It works, but it comes with friction:
- Every Context update re-renders every component that consumes that context, even ones that only care about a slice of the value — you have to split contexts carefully to avoid this.
- You still need
useStateoruseReducersomewhere to actually hold the value, plus a Provider component wrapping part of your tree. - Redux solves the re-render granularity problem, but at the cost of actions, reducers, a store setup, and (traditionally) a lot of files for what might be three pieces of state.
Zustand sits in between: a real store outside the React tree (like Redux), but defined in a few lines with no reducers, no action types, and no Provider wrapping required.
Creating a Store
npm install zustand
// store/useCartStore.js
import { create } from "zustand";
export const useCartStore = create((set) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
clear: () => set({ items: [] }),
}));
create takes a function that receives set (and optionally get) and returns the initial state plus any functions that update it. That’s the entire store — no separate reducer, no dispatched action objects, no switch statement.
Using the Store in Components
import { useCartStore } from "../store/useCartStore";
function CartBadge() {
// Only re-renders when `items.length` actually changes.
const itemCount = useCartStore((state) => state.items.length);
return <span>🛒 {itemCount}</span>;
}
function AddToCartButton({ product }) {
const addItem = useCartStore((state) => state.addItem);
return <button onClick={() => addItem(product)}>Add to cart</button>;
}
useCartStore is a hook — call it with a selector function, and the component only re-renders when the selected slice of state changes. CartBadge re-renders when items.length changes; it doesn’t care if some unrelated field in the store changes. There’s no Provider anywhere in the tree; both components import the same store directly and read from it.
Why This Feels Different From Redux
- No action types, no
dispatch, no reducer switch statement —addItemandremoveItemare just functions that callsetdirectly. - No boilerplate wiring — creating the store is the setup. There’s no separate store configuration file combining reducers, no middleware required to get started.
- State lives outside React, so components anywhere in the tree can read or update it without prop drilling or Context nesting.
- Selectors give you Redux-level render efficiency (only re-render on the slice you use) without
useSelector’s equality-check ceremony or connecting components manually.
When to Reach for Zustand vs. Context vs. Redux
useState/useReducer— state used by one component and maybe passed a level or two down via props. Simplest option; reach here first.- React Context — state that’s genuinely simple and changes rarely (a theme, a locale, feature flags) shared across a subtree. Fine for low-frequency updates; avoid it for state that changes often, since consumers re-render on every change by default.
- Zustand — shared, frequently-changing client state used across many components that aren’t necessarily nested in a clean subtree (cart contents, UI state like sidebar open/closed, multi-step form state, auth session flags). Minimal setup, good render performance via selectors, no Provider required.
- Redux (with Redux Toolkit) — large applications where you specifically want its ecosystem: time-travel debugging, a strict unidirectional action log, middleware for side effects, or a team convention already built around it. Redux Toolkit has significantly cut the historical boilerplate, but Zustand is still the lighter starting point for most new projects.
For a small-to-medium React app, or even a large one where the state itself isn’t the bottleneck, Zustand tends to be the pragmatic middle ground: real global state, minimal code, and no architecture decisions forced on you upfront. If you’re just getting comfortable with React’s own state model first, that’s a reasonable place to stay until you actually feel the pain global state is meant to solve.