How to Use Zustand for React State Management

Set up Zustand in a React app. Create a hook based store with the create function, read slices with selectors, and update state with set.

6 min read

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.

bashbash
npm install zustand

The 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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Does Zustand need a Provider?

No. A Zustand store is a hook, so any component can import and call it directly without wrapping the app.

What does the set function do?

set updates the store state and shallow merges the returned object into the current state. It also accepts a function that receives the previous state.

When should I not use Zustand?

Skip it for state that lives in one component. useState is simpler there. Also keep server cache in a data library, not in a Zustand store.

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.