Setting up Zustand in your React App

Setting up Zustand in your React App

Sheheryar PirzadaDecember 28, 2025
ReactNext.jsState ManagementTypeScriptZustand

"A global store with hooks, without Providers or reducers. Zustand scales from a single counter to app-wide state with minimal boilerplate."

Zustand is a small, fast state-management library for React. In this post I'll build a typed store from scratch, use it efficiently with selectors, then layer in persist and devtools middleware.

01

Install

Add Zustand to your React or Next.js project.

Terminal
npm i zustand
# or
yarn add zustand
# or
pnpm add zustand
02

Create a store

Define state and actions, then pass them to create. No reducers, no action types, no Provider required.

src/store/useCounterStore.ts
import { create } from "zustand";

type CounterState = {
  count: number;
  inc: () => void;
  dec: () => void;
  setCount: (next: number) => void;
  reset: () => void;
};

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
  dec: () => set((s) => ({ count: s.count - 1 })),
  setCount: (next) => set({ count: next }),
  reset: () => set({ count: 0 }),
}));

Zustand stores are module-level singletons by default. No wrapping your app in a Provider.

03

Use the store in a component

Call the hook and pick only what you need. Selectors ensure the component only re-renders when that specific slice changes.

Counter.tsx
import React from "react";
import { useCounterStore } from "../store/useCounterStore";

export function Counter() {
  const count = useCounterStore((s) => s.count);
  const inc   = useCounterStore((s) => s.inc);
  const dec   = useCounterStore((s) => s.dec);
  const reset = useCounterStore((s) => s.reset);

  return (
    <div className="flex items-center gap-3">
      <button onClick={dec}>-</button>
      <span>{count}</span>
      <button onClick={inc}>+</button>
      <button onClick={reset}>reset</button>
    </div>
  );
}

Tip: avoid calling useCounterStore() without a selector on large stores. It subscribes the component to the entire store and can cause unnecessary re-renders.

04

Add persistence and DevTools

Zustand ships middleware out of the box. persist saves state to localStorage. devtools connects to the Redux DevTools extension for time-travel debugging.

src/store/useCounterStore.ts (with middleware)
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

type CounterState = {
  count: number;
  inc: () => void;
  dec: () => void;
  reset: () => void;
};

export const useCounterStore = create<CounterState>()(
  devtools(
    persist(
      (set) => ({
        count: 0,
        inc:   () => set((s) => ({ count: s.count + 1 }), false, "counter/inc"),
        dec:   () => set((s) => ({ count: s.count - 1 }), false, "counter/dec"),
        reset: () => set({ count: 0 }, false, "counter/reset"),
      }),
      { name: "counter-store" }
    ),
    { name: "CounterStore" }
  )
);

The third argument to set is an action name for DevTools. Named actions make debugging significantly easier.

05

Common pitfalls

01
Client-only APIs in Next.jspersist uses localStorage, which doesn't exist during SSR. It's fine as long as the store is only read in the browser. If you render store state on the server, you may see hydration mismatches.
02
Over-subscribingPrefer selectors ((s) => s.count) so React only re-renders when that slice changes. Avoid calling the hook without a selector on large stores.
03
Store sprawlKeep stores domain-based (auth, UI, cart) instead of one mega-store as the app grows.
Wrap-up

State without the ceremony.

Zustand's sweet spot is how little ceremony it needs. Start with a tiny store, use selectors for performance, and add middleware as your app grows.

No boilerplate. No Provider. Just a hook and a store.

Zustand · State ManagementReact · Next.js · TypeScript