Zustand: Minimalist State Management
Managing global state in React has historically been a complex endeavor. Redux introduced robust patterns but came with heavy boilerplate (actions, reducers, types). Context API is built-in but suffers from performance issues (re-rendering the entire tree on updates) when used for high-frequency state.
Zustand (German for "state") is a small, fast, and scalable barebones state management solution. It uses a hook-based API and elegantly solves the boilerplate and performance issues of its predecessors.
Core Principles
- No Boilerplate: You don't need Context Providers wrapping your app.
- Selective Renders: Components only re-render if the specific piece of state they select changes.
- Immutability: While it expects immutable updates, it doesn't force complex reducers.
- Hooks First: The store is literally a custom React hook.
1. Creating a Store
Creating a store in Zustand is as simple as calling create and passing a function that returns your state and actions.
import { create } from 'zustand';
interface BearState {
bears: number;
increasePopulation: () => void;
removeAllBears: () => void;
}
export const useBearStore = create<BearState>((set) => ({
bears: 0,
// Actions are just functions that call set()
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
2. Using the Store
You can access the store anywhere in your component tree without Providers.
Crucial Pattern: Selectors Always pass a selector function to your hook to extract only what you need. This ensures your component only re-renders when that specific value changes.
import { useBearStore } from './store';
function BearCounter() {
// Selects ONLY the bears number. Ignores changes to other state.
const bears = useBearStore((state) => state.bears);
return <h1>{bears} around here ...</h1>;
}
function Controls() {
// Selects ONLY the action. This component will NEVER re-render!
const increasePopulation = useBearStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>one up</button>;
}
3. Async Actions
Zustand handles asynchronous data effortlessly. You don't need heavy middlewares like redux-thunk or redux-saga. Just make your action async.
const useUserStore = create((set) => ({
users: [],
loading: false,
fetchUsers: async () => {
set({ loading: true });
const response = await fetch('/api/users');
const data = await response.json();
set({ users: data, loading: false });
}
}));
4. Middleware: Persist & DevTools
Zustand comes with built-in middlewares that wrap your store for powerful features.
LocalStorage Persistence
Automatically save and load your store state to localStorage.
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export const useAuthStore = create(
persist(
(set) => ({
token: null,
login: (token) => set({ token }),
logout: () => set({ token: null }),
}),
{
name: 'auth-storage', // key in localStorage
storage: createJSONStorage(() => sessionStorage), // optional: use sessionStorage
}
)
);
Redux DevTools
You can use the official Redux DevTools browser extension to time-travel debug your Zustand store.
import { devtools } from 'zustand/middleware';
export const useStore = create(
devtools((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
}))
);
(Notice the third argument 'increment' passed to set. This names the action in the DevTools!)
Summary
Zustand provides the power of Redux without the headache. For most modern React applications, it hits the perfect sweet spot between the simplicity of useState and the robust architecture of global stores.