Skip to content
State Management

Zustand — Component Library Guide

A small, fast and scalable bearbones state-management solution. No boilerplate, no providers, just hooks. Works with Immer, DevTools, and middleware.

★ Editor's Pick TypeScript MIT
5.1M weekly downloads
48.2k GitHub stars
~1kB bundle size
v5.0.3 latest version
stateglobal-statehooksminimaldevtools

TL;DR: Zustand is minimal global state for React. Define a store with create(), use it anywhere with a hook - no Provider, no boilerplate. Subscribe to only the slices you need to avoid unnecessary re-renders. ~1kB. Official docs: zustand-demo.pmnd.rs.

What is Zustand?

Zustand is a minimal global state manager for React. You define a store - a plain object with state and actions - and subscribe to it in any component with a hook. No Provider required, no boilerplate, no reducers.

At ~1kB gzipped, it's hard to justify anything heavier for client state management.

I moved a mid-size dashboard off Redux Toolkit to Zustand a while back, mostly out of curiosity about how much boilerplate would actually disappear. The store file went from 340 lines of actions, reducers, and selectors to about 60 lines of plain functions. Nobody on the team missed the action-type constants.

When to use it

Use Zustand for client-side global state that multiple components need to share:

  • Authentication state (user, token)
  • UI state (sidebar open, theme, modal queues)
  • Multi-step wizard state shared across routes
  • Shopping cart, notification queue

Don't use it for server state (API responses) - that's TanStack Query's job. Don't use it for local component state - that's useState.

Zustand vs Jotai: Zustand is store-first (one object per domain); Jotai is atom-first (one hook per value). Zustand is easier to reason about for most teams.

Key Features

  • No Provider - stores work outside React tree, usable in vanilla JS too
  • Immer integration - immer middleware for mutation-style updates
  • Redux DevTools - inspect state history in browser devtools
  • Persist middleware - sync state to localStorage or sessionStorage
  • Subscriptions - subscribe to store changes outside React

Installation

npm install zustand

Basic Usage

import { create } from 'zustand'

interface CartStore {
  items: CartItem[]
  addItem: (item: CartItem) => void
  removeItem: (id: string) => void
  clear: () => void
}

const useCartStore = create<CartStore>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
  clear: () => set({ items: [] }),
}))

// In any component:
function CartCount() {
  const count = useCartStore((state) => state.items.length)
  return <span>{count}</span>
}

Using Immer for Complex Updates

import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

const useStore = create<State>()(immer((set) => ({
  users: [],
  updateUser: (id, patch) => set((state) => {
    const user = state.users.find(u => u.id === id)
    if (user) Object.assign(user, patch)  // Immer lets you mutate directly
  }),
})))

Persist State to localStorage

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

const useSettingsStore = create<Settings>()(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    { name: 'app-settings' }  // localStorage key
  )
)

Subscribing Outside React

Sometimes you need to react to a state change without a component in the picture, an analytics call, a WebSocket reconnect, a toast triggered from a non-React module. subscribe handles that without turning the store into an event emitter you have to wire up yourself:

import { useCartStore } from './cart-store'

const unsubscribe = useCartStore.subscribe((state, prevState) => {
  if (state.items.length > prevState.items.length) {
    trackEvent('cart_item_added', { total: state.items.length })
  }
})

// later, e.g. on unmount or route change
unsubscribe()

This runs outside the React render cycle entirely, so it's safe to call inside useEffect cleanup, a route change handler, or a plain script that never touches JSX.

TypeScript Tips

Always type your store interface separately - it enables reuse:

interface AuthStore {
  user: User | null
  token: string | null
  login: (user: User, token: string) => void
  logout: () => void
}

const useAuthStore = create<AuthStore>()((set) => ({
  user: null,
  token: null,
  login: (user, token) => set({ user, token }),
  logout: () => set({ user: null, token: null }),
}))

Always select only what you need - subscribing to the whole store re-renders on every change:

// ✅ Re-renders only when user.name changes
const name = useAuthStore((s) => s.user?.name)

// ❌ Re-renders on every store mutation
const store = useAuthStore()

Common Gotchas

  • Object slices - if you select an object (s => s.filters), Zustand uses reference equality. Adding shallow from zustand/shallow prevents unnecessary re-renders when the object contents change but not the reference
  • Async actions - you can call set from inside async functions directly; no special middleware needed
  • Testing - reset store state between tests with useStore.setState(initialState) in beforeEach
  • Jotai - atom-based alternative to Zustand; better for fine-grained subscriptions, Zustand is better for domain stores
  • TanStack Query - use alongside Zustand: TanStack Query for server/async state, Zustand for client UI state
  • TypeScript Generics Guide - type your Zustand stores with generic interfaces for full create<StoreType>() safety

Frequently Asked Questions

Do I need a Provider to use Zustand?

No. Zustand stores are module-level singletons. You import the hook and use it directly in any component without wrapping your app in a Provider. You can optionally use a Provider for isolated store instances, for example in tests.

What is the difference between Zustand and Redux Toolkit?

Zustand has virtually no boilerplate - define a store in one function call. Redux Toolkit is more structured with slices, reducers, and selectors. Zustand is better for small to medium apps. Redux Toolkit is useful when you need strict architectural conventions or time-travel debugging across a large team.

How do I persist Zustand state to localStorage?

Wrap your store creator with the persist middleware from zustand/middleware. Provide a name property which becomes the localStorage key. By default all state is persisted; use the partialize option to save only specific fields.

Can I use Zustand outside of React components?

Yes. Call store.getState() to read state and store.setState() to update it anywhere in your JavaScript code - event listeners, API callbacks, utility functions - without needing a React component.

How do I avoid unnecessary re-renders with Zustand?

Always select only the specific piece of state you need rather than the entire store. For object slices, import shallow from zustand/shallow and pass it as the equality function to prevent re-renders when object contents change but the reference does not.