React Zustand Guide: Lightweight Global State (2026)

Zustand is a small, fast global state library for React. Unlike Redux, there's no boilerplate — no actions, reducers, or Provider wrapping. You define a store as a hook, access state anywhere and update it with simple functions. Despite its simplicity, Zustand scales well and supports middleware like persistence, devtools and Immer integration.

Creating a Store

npm install zustand
import { create } from 'zustand'

// Define store with state + actions
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  incrementBy: (amount) => set((state) => ({ count: state.count + amount })),
}))

// Use anywhere — no Provider needed
function Counter() {
  const { count, increment, decrement, reset } = useCounterStore()
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Selectors and Performance

Components only re-render when their selected slice of state changes:

// ✅ Select only what you need — re-renders only when count changes
function CountDisplay() {
  const count = useCounterStore((state) => state.count)
  return <span>{count}</span>
}

// ✅ Select action references — actions are stable, no re-renders
function IncrementButton() {
  const increment = useCounterStore((state) => state.increment)
  return <button onClick={increment}>+</button>
}

// ❌ Destructuring without selector — re-renders on any state change
function BadComponent() {
  const { count, increment } = useCounterStore()  // Avoid this
}

For computed values derived from multiple state fields, use a selector with shallow comparison:

import { useShallow } from 'zustand/react/shallow'

// Re-renders only when firstName OR lastName changes
function UserName() {
  const { firstName, lastName } = useUserStore(
    useShallow((state) => ({ firstName: state.firstName, lastName: state.lastName }))
  )
  return <span>{firstName} {lastName}</span>
}

Async Actions

const useUserStore = create((set) => ({
  user: null,
  isLoading: false,
  error: null,

  fetchUser: async (userId) => {
    set({ isLoading: true, error: null })
    try {
      const res = await fetch(`/api/users/${userId}`)
      const user = await res.json()
      set({ user, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },

  updateUser: async (updates) => {
    // Optimistic update
    set((state) => ({ user: { ...state.user, ...updates } }))
    try {
      await fetch('/api/users', { method: 'PATCH', body: JSON.stringify(updates) })
    } catch {
      // Rollback on failure — refetch
      useUserStore.getState().fetchUser(useUserStore.getState().user.id)
    }
  },
}))

TypeScript

import { create } from 'zustand'

interface BearState {
  bears: number
  honey: number
  addBear: () => void
  removeAllBears: () => void
  collectHoney: (amount: number) => void
}

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  honey: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  collectHoney: (amount) => set((state) => ({ honey: state.honey + amount })),
}))

// TypeScript infers types — fully typed selectors
const bears = useBearStore((state: BearState) => state.bears)

Middleware: devtools, persist, immer

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

// Combine middleware (order matters — devtools outermost)
const useStore = create(
  devtools(
    persist(
      immer((set) => ({
        todos: [],
        addTodo: (text) => set((state) => {
          // Immer allows direct mutation
          state.todos.push({ id: Date.now(), text, done: false })
        }),
        toggleTodo: (id) => set((state) => {
          const todo = state.todos.find(t => t.id === id)
          if (todo) todo.done = !todo.done
        }),
        removeTodo: (id) => set((state) => {
          state.todos = state.todos.filter(t => t.id !== id)
        }),
      })),
      {
        name: 'todos-storage',           // localStorage key
        partialize: (state) => ({ todos: state.todos }),  // Only persist todos
      }
    ),
    { name: 'TodoStore' }  // DevTools label
  )
)

Custom persist storage (sessionStorage or custom):

persist(storeCreator, {
  name: 'my-store',
  storage: {
    getItem: (key) => sessionStorage.getItem(key),
    setItem: (key, value) => sessionStorage.setItem(key, value),
    removeItem: (key) => sessionStorage.removeItem(key),
  },
})

Slices Pattern

Split large stores into slices for better organization:

// slices/authSlice.ts
export const createAuthSlice = (set, get) => ({
  user: null,
  token: null,
  login: async (credentials) => {
    const { user, token } = await apiLogin(credentials)
    set({ user, token })
  },
  logout: () => set({ user: null, token: null }),
})

// slices/cartSlice.ts
export const createCartSlice = (set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clearCart: () => set({ items: [] }),
  get total() {
    return get().items.reduce((sum, item) => sum + item.price * item.qty, 0)
  },
})

// store/index.ts — combine all slices
import { create } from 'zustand'
import { createAuthSlice } from './slices/authSlice'
import { createCartSlice } from './slices/cartSlice'

export const useStore = create((...args) => ({
  ...createAuthSlice(...args),
  ...createCartSlice(...args),
}))

// Use specific slices
const user = useStore((s) => s.user)
const cartTotal = useStore((s) => s.total)

Accessing Store Outside React

const useUserStore = create((set, get) => ({
  user: null,
  setUser: (user) => set({ user }),
}))

// Read state outside a component
const currentUser = useUserStore.getState().user

// Update state outside a component (e.g., in an interceptor)
useUserStore.getState().setUser(null)  // logout on 401

// Subscribe to changes outside React
const unsub = useUserStore.subscribe(
  (state) => state.user,
  (user, prevUser) => {
    if (!user && prevUser) {
      router.push('/login')
    }
  }
)

Testing

// Reset store between tests
import { act } from '@testing-library/react'

beforeEach(() => {
  useCounterStore.setState({ count: 0 })
})

test('increment increases count', () => {
  const { increment } = useCounterStore.getState()
  act(() => increment())
  expect(useCounterStore.getState().count).toBe(1)
})

// Mock store in component tests
test('renders correct count', () => {
  useCounterStore.setState({ count: 42 })
  const { getByText } = render(<Counter />)
  expect(getByText('Count: 42')).toBeInTheDocument()
})