Zustand is a small state management library for React built around hooks. You create a store once and call it from any component with a selector, with no provider wrapper.
It holds client state that several components share, and it leaves server cache to data libraries. This tutorial builds a counter and a cart from two small stores.
Install Zustand
Zustand is a single package with no peer dependencies beyond React. It works with any React setup and needs no provider or store configuration at the root.
npm install zustandThe create function is the entire setup API. There is no provider, no reducer boilerplate, and no context to configure. You can drop it into a Vite app or any component file without changing the entry point.
Create a store
A Zustand store is a hook. Call create with a function that receives set and returns the initial state plus the actions that change it.
import { create } from "zustand";
export const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));The set function updates state and, by default, shallow merges the object you return into the existing state. The functional form receives the current state, which matters when the next value depends on the previous one.
The store can live in its own file and be imported anywhere. Because it is a hook, importing it in many components shares one backing state, not copies.
Read state with a selector
Components call the store hook with a selector. Zustand compares the selected value and re-renders the component only when that value changes.
export function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
<div>
<button onClick={increment}>+</button>
<span>{count}</span>
</div>
);
}The first selector returns count, so the component re-renders when count changes. The second returns the increment function, which stays stable across renders and does not cause extra work.
Select only what you need
A selector should return one small value, not a new object built on every call. Returning a fresh object from a selector makes Zustand compare two new references, which can re-render on every store change.
const { count, reset } = useCounterStore();This reads the whole store and re-renders when anything in it changes. Prefer two single value selectors when each part is used by a different component. A component can also select several values by calling the hook once per value.
Update state with set
Actions live on the store and call set from inside a handler. They are plain functions, so a store can hold a number, an object, or a list.
export const useCartStore = create((set) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
}));The functional update appends to the existing array without mutating it. The spread keeps the old items and adds the new one, so the array can be compared by reference. This matches the React rule that state updates stay immutable.
Add DevTools middleware
Zustand works with the Redux DevTools browser extension through a middleware wrapper. Wrap the store to see every set call and state change while you develop.
import { create } from "zustand";
import { devtools } from "zustand/middleware";
export const useCounterStore = create(devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
})));The devtools wrapper logs each update, which makes it easier to find where a value changed. Middleware is optional and wraps the store logic without touching the components that read it.
When Zustand is the right fit
Zustand fits apps that want shared state without Redux style structure. For state that stays inside one component, useState remains simpler, so do not reach for a store too early.
A broader comparison with built in state is in the state management decision guide, and the tradeoffs against Redux Toolkit are in Zustand vs Redux Toolkit. If you are choosing between context and a library, see context vs Redux vs Zustand.
Zustand also manages client state, not server cache. Fetching, caching, and revalidating belong in a data library such as TanStack Query, which keeps server data fresh without stuffing it into a store. Keeping the two apart makes both easier to reason about.
Rune AI
Key Insights
- Install zustand and import the create function.
- A store is a hook that returns state and actions.
- Select single values so components re-render only when those change.
- Use functional updates in set when the next value depends on the previous one.
Frequently Asked Questions
Does Zustand need a Provider?
What does the set function do?
When should I not use Zustand?
Conclusion
Zustand gives you shared state with almost no setup. Create a store with the create function, read slices with selectors, and update values with set, all without a provider.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.