Zustand

Master Zustand — the lightweight, modern state management library that most teams should use instead of Redux. Learn store design, selectors, persistence, and enterprise patterns.

Last reviewed: January 2025

·React 16+React 18React 19Zustand 4.x
✅
Quick Understanding: Zustand is a tiny (1KB) state management library that gives you a global store without the boilerplate of Redux. You define state and actions in one place, and any component can subscribe to exactly the slice it needs. It is the recommended choice for most React apps in 2024.

🔍 What Is It?

Zustand provides a simple API to create a global state store:

const useStore = create<State>((set, get) => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
}));
  • →create — creates a hook that components use to subscribe to the store
  • →set — updates state (shallow merges by default, like setState)
  • →get — reads current state inside actions (useful for derived logic)
  • →Selectors — useStore(state => state.count) subscribes to only the count field; the component only re-renders when count changes

🎯 Why Do We Need It?

Context API causes all consumers to re-render when the value changes. Redux requires actions, action types, reducers, and selectors for every piece of state. Zustand sits in the sweet spot:

  • →No boilerplate — define state and actions in one function
  • →Selector-based subscriptions — components only re-render when their specific slice changes
  • →No Provider needed — the store is a module-level singleton
  • →Works outside React — actions can be called from anywhere (event handlers, utility functions, other stores)
  • →1KB — negligible bundle impact

🏗️ What Happens in Real Projects?

Zustand is ideal for:

  • →Shopping cart — items, quantities, totals, add/remove actions
  • →UI state — sidebar open/closed, active modal, selected rows in a table
  • →Filter state — search query, selected filters, sort order shared across a page
  • →User preferences — theme, language, notification settings (with persist middleware)
  • →Multi-step forms — form data persisted across steps

⚙️ How It Works

Zustand uses a pub/sub model. The store is a JavaScript object. When you call set, Zustand:

  1. Merges the new state shallowly
  2. Notifies all subscribers
  3. Each subscriber runs its selector function
  4. If the selected value changed (by reference), the component re-renders

No Provider:

Unlike Context, Zustand stores are module-level singletons. You import the hook directly. This makes it simpler to use but also means all instances of the app share the same store (relevant for SSR/testing).

💻 Example

Zustand in a Real Application — Store definition, selectors, async actions, and persistence.

tsx
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  // Derived getters
  totalItems: () => number;
  totalPrice: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],

      addItem: (item) => set(state => {
        const existing = state.items.find(i => i.id === item.id);
        if (existing) {
          return { items: state.items.map(i =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
          )};
        }
        return { items: [...state.items, { ...item, quantity: 1 }] };
      }),

      removeItem: (id) => set(state => ({
        items: state.items.filter(i => i.id !== id),
      })),

      updateQuantity: (id, quantity) => set(state => ({
        items: quantity <= 0
          ? state.items.filter(i => i.id !== id)
          : state.items.map(i => i.id === id ? { ...i, quantity } : i),
      })),

      clearCart: () => set({ items: [] }),

      totalItems: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
      totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
    }),
    { name: 'cart-storage' } // persists to localStorage
  )
);

// Component — subscribes only to items.length, not the full items array
function CartIcon() {
  const count = useCartStore(state => state.totalItems());
  return <button>Cart ({count})</button>;
}

🤔 Common Confusions

❓ Does Zustand need a Provider?

No. Zustand stores are module-level singletons. You just import the hook and use it anywhere. This is simpler than Context/Redux but means all app instances share the same store — relevant for SSR (use createStore instead of create for SSR-safe stores) and testing (reset the store between tests).

❓ How do I prevent unnecessary re-renders with Zustand?

Use selectors: useStore(state => state.count) instead of useStore(). With a selector, the component only re-renders when the selected value changes. Without a selector, the component re-renders on any store update. For objects, use the shallow equality helper: useStore(state => state.user, shallow).

❓ Should I put everything in one Zustand store?

No. Create separate stores per concern: useCartStore, usePolicyStore, useUIStore. This keeps stores focused, easier to test, and prevents unrelated updates from triggering re-renders across unrelated consumers.

🚨 Common Production Issues

**Re-renders without selectors** — useStore() without a selector subscribes to the entire store. Any update re-renders the component. Always use selectors.

**Object selector instability** — useStore(state => ({ a: state.a, b: state.b })) creates a new object every render. Fix: use the shallow import from zustand/shallow.

**SSR issues** — Module-level stores cause hydration mismatches in Next.js. Use createStore with a React context wrapper for SSR-safe Zustand.

**Testing** — Zustand stores persist between tests. Reset the store in beforeEach: useStore.setState(initialState).

✅ Best Practices

Always use selectors to subscribe to specific slices of state

Use shallow from zustand/shallow for object selectors

Separate stores by domain concern — not one giant store

Use the persist middleware for state that should survive page refreshes

Reset stores in test beforeEach hooks

For SSR (Next.js), use the context pattern for Zustand stores

Keep actions close to the state they modify — define them inside the store

🏛️ Architect's Note

Senior Architect's Perspective

Zustand is the right default choice for state management in 2024. It is small, simple, and powerful enough for the vast majority of React applications. The pattern I use in every project: Context for dependency injection (auth, theme), Zustand for application state (cart, filters, UI), React Query for server state (API data). This three-layer approach covers every state management need without the overhead of Redux.

💬 Frequently Asked Questions

Can I use Zustand with TypeScript?

Yes, and it has excellent TypeScript support. Define your store interface and pass it as a generic: create<StoreType>()((set) => ({...})). Note the double parentheses — this is required for TypeScript inference to work correctly.

How does Zustand compare to Jotai and Recoil?

Zustand uses a single store with actions (Redux-like but simpler). Jotai and Recoil use atomic state — small independent atoms that compose together (more like useState but global). Zustand is better for complex state with many actions. Jotai is better for independent pieces of global state. Both are excellent; Zustand has more adoption and a larger ecosystem.

🎯 Key Takeaways

1Zustand is a 1KB global state manager with no boilerplate and no Provider needed
2Always use selectors to subscribe to specific slices — prevents unnecessary re-renders
3Use shallow for object selectors to prevent reference-based re-renders
4Separate stores by domain — not one giant store
5persist middleware for localStorage persistence
6The recommended stack: Context (injection) + Zustand (app state) + React Query (server state)

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these React references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide React learning path

Browse the full React topic collection and learning order on SplashRide.

React Interview Prep

Practice React interview questions and scenario-based answers.

Frontend Engineer Career Path

A beginner-friendly journey through web foundations, React, production frontend engineering, and interview preparation.

Python Developer Career Path

A practical route through Python fundamentals, automation, backend basics, testing, projects, interview preparation, and career growth.

Context API

Master React Context — the built-in solution for sharing state across the component tree without prop drilling. Learn when to use it, when not to, and how senior engineers implement it in production.

Redux Toolkit

Master Redux Toolkit — the modern, opinionated way to write Redux. Learn slices, thunks, RTK Query, and when Redux is actually the right choice for enterprise React applications.