Setting up Zustand in your React App

Setting up Zustand in your React App

Sheheryar PirzadaDecember 28, 20258 min read
ReactNext.jsState ManagementTypeScriptZustand

Zustand is a small, fast state-management library for React. It gives you a global store with hooks, without Providers or reducers, and it scales from “a single counter” to “app-wide state + persistence + devtools” with minimal boilerplate.

In this post we’ll build a tiny store, type it with TypeScript, use it efficiently with selectors, and then add persist + devtools.

Step 1 — Install

Install Zustand in your React/Next.js project:

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

Step 2 — Create a store (TypeScript-friendly)

Create a store file (example: src/store/useCounterStore.ts). The core idea is simple: define state + actions, then call create.

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 }),
}));

Notice: no reducers, no action types, no Provider. Zustand stores are module-level singletons by default.

Step 3 — Use the store in a component

In components, call the hook and pick what you need. Use selectors so components only re-render when that slice changes.

Example component
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} className="px-3 py-2 rounded bg-zinc-200 dark:bg-neutral-800">
        -
      </button>
      <span className="min-w-10 text-center font-medium">{count}</span>
      <button onClick={inc} className="px-3 py-2 rounded bg-zinc-200 dark:bg-neutral-800">
        +
      </button>
      <button onClick={reset} className="px-3 py-2 rounded bg-zinc-200 dark:bg-neutral-800">
        reset
      </button>
    </div>
  );
}

Tip: avoid useCounterStore() without a selector for large stores; it subscribes the component to the entire store and can cause extra re-renders.

Step 4 — Add persistence + Redux DevTools

Zustand ships middleware. Two of the most useful are persist (save to localStorage) and devtools (debug state changes in the Redux DevTools extension).

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, which makes debugging way nicer.

Common pitfalls (and how to avoid them)

Client-only APIs in Next.js: persist 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 mismatch.

Over-subscribing: prefer selectors ((s) => s.count) so React only re-renders when that slice changes.

Store sprawl: keep stores domain-based (auth, UI, cart) instead of “one mega-store” once the app grows.

Wrap-up

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