Minimal Boilerplate
No reducers, action types, provider nesting, or heavy setup just to store a few values.
Zustand is a small, fast, and scalable state management library for React.
It provides a central store (single source of truth) where shared data, update logic, and derived behavior can live together.
Minimal Boilerplate
No reducers, action types, provider nesting, or heavy setup just to store a few values.
Hook-Based API
Everything works using React hooks, so reading and updating state feels like normal React code.
Selective Re-rendering
Components only re-render when the selected slice changes, not when unrelated store values change.
Production Ready
Works well in real-world apps with middleware for persistence, logging, and DevTools.
State management means tracking the current truth of your application data.
Example idea:
Same happens in apps: Different components may show different values.
These terms show up throughout Zustand docs and code:
courses, count, or themecreate() that holds state and actionsaddCourse or toggleThemestate => state.coursesThe important production idea is simple: store only shared app state here, not every local input value.
Zustand follows a simplified Flux pattern:
You do not dispatch action objects or write reducers for normal Zustand usage. You call a function directly, and that function updates the store.
npm install zustandimport { create } from "zustand";
const useCourseStore = create((set) => ({ courses: [],
addCourse: (course) => set((state) => ({ courses: [course, ...state.courses], })),
removeCourse: (id) => set((state) => ({ courses: state.courses.filter((c) => c.id !== id), })),
toggleCourseStatus: (id) => set((state) => ({ courses: state.courses.map((course) => (course.id === id ? { ...course, completed: !course.completed } : course)), })),}));
export default useCourseStore;create() → creates the store hook and wires React components to the storecourses → the initial state value exposed by the storeset() → updates state immutably and notifies subscribed componentsset() is usually enough for updates. Use get() only when you need to read the current store value inside an action before deciding what to write.
setset is used to update state.
set((state) => ({ courses: [...state.courses, newCourse],}));state → the latest snapshot of the store at the moment of the updateExample with get() when you need the current value directly:
const useCourseStore = create((set, get) => ({ courses: [],
addCourseIfMissing: (course) => { const exists = get().courses.some((item) => item.id === course.id); if (exists) return;
set((state) => ({ courses: [course, ...state.courses], })); },}));const courses = useCourseStore((state) => state.courses);This is the subscription model: the component subscribes only to courses, so it re-renders when courses changes.
const addCourse = useCourseStore((state) => state.addCourse);This is better than pulling the entire store into the component, because the component does not care about every other field.
const addCourse = useCourseStore((state) => state.addCourse);const store = useCourseStore();This subscribes the component to the entire store. Any state change can re-render it, even if the component only uses one field.
const { courses, addCourse } = useCourseStore();This is also broad because it reads the whole store. It looks convenient, but it often defeats Zustand’s biggest performance benefit.
const courses = useCourseStore((state) => state.courses);const addCourse = useCourseStore((state) => state.addCourse);For multiple fields, use one selector with a shallow comparison helper when available, or split selectors if the component is small.
function CourseForm() { const [title, setTitle] = useState(""); const addCourse = useCourseStore((state) => state.addCourse);
function handleSubmit() { if (!title.trim()) return;
addCourse({ id: Math.ceil(Math.random() * 1000000), title, completed: false, });
setTitle(""); }
return ( <> <input value={title} onChange={(e) => setTitle(e.target.value)} /> <button onClick={handleSubmit}>Add</button> </> );}const { courses, removeCourse, toggleCourseStatus } = useCourseStore((state) => ({ courses: state.courses, removeCourse: state.removeCourse, toggleCourseStatus: state.toggleCourseStatus,}));This pattern is fine when you need several fields together. In larger components, prefer separate selectors or shallow comparison to keep rerenders predictable.
Zustand supports middleware for advanced features.
Middleware wraps the store and adds behavior without changing how components use the hook.
import { createJSONStorage, persist } from "zustand/middleware";
const useStore = create( persist( (set) => ({ courses: [], }), { name: "courses", storage: createJSONStorage(() => localStorage), }, ),);Use localStorage when the data should survive browser restarts, such as theme, preferences, and saved drafts.
import { createJSONStorage, persist } from "zustand/middleware";
const useStore = create( persist( (set) => ({ courses: [], }), { name: "courses", storage: createJSONStorage(() => sessionStorage), }, ),);Use sessionStorage when the data should disappear when the tab closes, such as temporary form progress or one-session flows.
| Storage | Lifetime | Best for | Not ideal for |
|---|---|---|---|
| localStorage | Persists until cleared | Theme, auth UI flags, persisted filters | Sensitive secrets, short-lived state |
| sessionStorage | Clears when the tab closes | Wizard steps, ephemeral drafts, temporary UI state | Data that must survive browser restarts |
partialize to avoid storing large or sensitive dataversion and migrate when the store shape changesconst useStore = create( persist( (set) => ({ theme: "light", courses: [], }), { name: "app-store", storage: createJSONStorage(() => localStorage), partialize: (state) => ({ theme: state.theme }), version: 1, }, ),);import { devtools } from "zustand/middleware";
const useStore = create( devtools((set) => ({ count: 0, increase: () => set((s) => ({ count: s.count + 1 })), })),);The devtools middleware connects your store to the Redux DevTools browser extension. In Chrome, install the Redux DevTools extension, then open the browser DevTools to inspect Zustand state, action names, and time-travel updates.
Why it matters in production:
| Feature | Zustand | Context API | Redux Toolkit |
|---|---|---|---|
| Boilerplate | Very Low | Low | Medium |
| Performance | High | Medium | High |
| Learning Curve | Easy | Easy | Medium |
| Best Use | Medium apps | Small apps | Large complex apps |
For larger apps, keep store files focused on domain logic and keep components focused on rendering. That separation makes Zustand easier to maintain than a giant catch-all state file.