JavaScript 9 min read

Zustand: Lightweight React State Management Made Simple

Master Zustand for React state management. Learn store creation, async actions, middleware, persistence, and TypeScript integration patterns.

MR

Moshiour Rahman

· Updated May 9, 2026

What is Zustand?

Zustand is a small, fast, and scalable state management library for React. It offers a simpler API than Redux while providing powerful features like middleware and devtools support.

Why Zustand?

FeatureZustandReduxContext
Bundle Size~1KB~10KBBuilt-in
BoilerplateMinimalHighLow
Learning CurveEasySteepEasy
PerformanceExcellentGoodCan be slow
DevToolsYesYesLimited

Getting Started

Installation

npm install zustand

Basic Store

import { create } from 'zustand';

// Define store
interface CounterStore {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));

// Use in component
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>
  );
}

Selecting State

// Select single value (component only re-renders when count changes)
function CountDisplay() {
  const count = useCounterStore((state) => state.count);
  return <p>Count: {count}</p>;
}

// Select multiple values
function Controls() {
  const { increment, decrement } = useCounterStore((state) => ({
    increment: state.increment,
    decrement: state.decrement
  }));

  return (
    <>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
}

// Shallow comparison for objects
import { shallow } from 'zustand/shallow';

function UserInfo() {
  const { name, email } = useUserStore(
    (state) => ({ name: state.name, email: state.email }),
    shallow
  );
  return <p>{name} - {email}</p>;
}

Real-World Store Patterns

Todo Store

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

interface TodoStore {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
  addTodo: (text: string) => void;
  toggleTodo: (id: string) => void;
  deleteTodo: (id: string) => void;
  setFilter: (filter: 'all' | 'active' | 'completed') => void;
  clearCompleted: () => void;
}

const useTodoStore = create<TodoStore>((set) => ({
  todos: [],
  filter: 'all',

  addTodo: (text) => set((state) => ({
    todos: [
      ...state.todos,
      { id: crypto.randomUUID(), text, completed: false }
    ]
  })),

  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  })),

  deleteTodo: (id) => set((state) => ({
    todos: state.todos.filter((todo) => todo.id !== id)
  })),

  setFilter: (filter) => set({ filter }),

  clearCompleted: () => set((state) => ({
    todos: state.todos.filter((todo) => !todo.completed)
  }))
}));

// Derived state with selector
const useFilteredTodos = () => useTodoStore((state) => {
  switch (state.filter) {
    case 'active':
      return state.todos.filter((t) => !t.completed);
    case 'completed':
      return state.todos.filter((t) => t.completed);
    default:
      return state.todos;
  }
});

Auth Store

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthStore {
  user: User | null;
  token: string | null;
  isLoading: boolean;
  error: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  checkAuth: () => Promise<void>;
}

const useAuthStore = create<AuthStore>((set, get) => ({
  user: null,
  token: null,
  isLoading: false,
  error: null,

  login: async (email, password) => {
    set({ isLoading: true, error: null });

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });

      if (!response.ok) {
        throw new Error('Invalid credentials');
      }

      const { user, token } = await response.json();
      set({ user, token, isLoading: false });
      localStorage.setItem('token', token);

    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Login failed',
        isLoading: false
      });
    }
  },

  logout: () => {
    localStorage.removeItem('token');
    set({ user: null, token: null });
  },

  checkAuth: async () => {
    const token = localStorage.getItem('token');
    if (!token) return;

    set({ isLoading: true });

    try {
      const response = await fetch('/api/me', {
        headers: { Authorization: `Bearer ${token}` }
      });

      if (response.ok) {
        const user = await response.json();
        set({ user, token, isLoading: false });
      } else {
        localStorage.removeItem('token');
        set({ isLoading: false });
      }
    } catch {
      set({ isLoading: false });
    }
  }
}));

Shopping Cart Store

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

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

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

  addItem: (product) => set((state) => {
    const existing = state.items.find((item) => item.id === product.id);

    if (existing) {
      return {
        items: state.items.map((item) =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      };
    }

    return { items: [...state.items, { ...product, quantity: 1 }] };
  }),

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

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

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

  // Computed values using get()
  totalItems: () => get().items.reduce((sum, item) => sum + item.quantity, 0),

  totalPrice: () => get().items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  )
}));

Async Actions

Fetching Data

interface Post {
  id: number;
  title: string;
  body: string;
}

interface PostStore {
  posts: Post[];
  isLoading: boolean;
  error: string | null;
  fetchPosts: () => Promise<void>;
  createPost: (post: Omit<Post, 'id'>) => Promise<void>;
}

const usePostStore = create<PostStore>((set, get) => ({
  posts: [],
  isLoading: false,
  error: null,

  fetchPosts: async () => {
    set({ isLoading: true, error: null });

    try {
      const response = await fetch('/api/posts');
      const posts = await response.json();
      set({ posts, isLoading: false });
    } catch (error) {
      set({
        error: 'Failed to fetch posts',
        isLoading: false
      });
    }
  },

  createPost: async (post) => {
    set({ isLoading: true });

    try {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(post)
      });
      const newPost = await response.json();

      set((state) => ({
        posts: [...state.posts, newPost],
        isLoading: false
      }));
    } catch (error) {
      set({ error: 'Failed to create post', isLoading: false });
    }
  }
}));

Middleware

Persist Middleware

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

interface SettingsStore {
  theme: 'light' | 'dark';
  language: string;
  notifications: boolean;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (language: string) => void;
  toggleNotifications: () => void;
}

const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      theme: 'light',
      language: 'en',
      notifications: true,
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      toggleNotifications: () => set((state) => ({
        notifications: !state.notifications
      }))
    }),
    {
      name: 'settings-storage',
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({
        theme: state.theme,
        language: state.language
      })  // Only persist these fields
    }
  )
);

DevTools Middleware

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create<Store>()(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set(
        (state) => ({ count: state.count + 1 }),
        false,
        'increment'  // Action name for DevTools
      )
    }),
    { name: 'MyStore' }
  )
);

Immer Middleware

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

interface Store {
  users: { id: string; name: string; settings: { theme: string } }[];
  updateUserTheme: (userId: string, theme: string) => void;
}

const useStore = create<Store>()(
  immer((set) => ({
    users: [],
    updateUserTheme: (userId, theme) => set((state) => {
      const user = state.users.find((u) => u.id === userId);
      if (user) {
        user.settings.theme = theme;  // Direct mutation with Immer
      }
    })
  }))
);

Combining Middleware

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

const useStore = create<Store>()(
  devtools(
    persist(
      immer((set) => ({
        // Store definition
      })),
      { name: 'my-storage' }
    ),
    { name: 'MyStore' }
  )
);

Slices Pattern

Splitting Large Stores

// userSlice.ts
export interface UserSlice {
  user: User | null;
  setUser: (user: User) => void;
  clearUser: () => void;
}

export const createUserSlice = (set: any): UserSlice => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null })
});

// cartSlice.ts
export interface CartSlice {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  clearCart: () => void;
}

export const createCartSlice = (set: any): CartSlice => ({
  items: [],
  addItem: (item) => set((state: any) => ({
    items: [...state.items, item]
  })),
  clearCart: () => set({ items: [] })
});

// store.ts
import { create } from 'zustand';
import { createUserSlice, UserSlice } from './userSlice';
import { createCartSlice, CartSlice } from './cartSlice';

type Store = UserSlice & CartSlice;

const useStore = create<Store>()((...args) => ({
  ...createUserSlice(...args),
  ...createCartSlice(...args)
}));

export default useStore;

Testing

Testing Stores

import { act, renderHook } from '@testing-library/react';
import { useCounterStore } from './counterStore';

describe('Counter Store', () => {
  beforeEach(() => {
    // Reset store before each test
    useCounterStore.setState({ count: 0 });
  });

  it('should increment count', () => {
    const { result } = renderHook(() => useCounterStore());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it('should decrement count', () => {
    useCounterStore.setState({ count: 5 });

    const { result } = renderHook(() => useCounterStore());

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });
});

Mocking Stores

// __mocks__/store.ts
import { create } from 'zustand';

const mockStore = create(() => ({
  user: { id: '1', name: 'Test User' },
  isLoading: false,
  login: jest.fn(),
  logout: jest.fn()
}));

export default mockStore;

// In test file
jest.mock('./store');

Best Practices

1. Keep Stores Focused

// Good: Separate concerns
const useAuthStore = create<AuthStore>(...);
const useCartStore = create<CartStore>(...);
const useUIStore = create<UIStore>(...);

// Avoid: One giant store
const useStore = create<EverythingStore>(...);

2. Use Selectors

// Good: Only re-renders when count changes
const count = useStore((state) => state.count);

// Avoid: Re-renders on any state change
const state = useStore();

3. Memoize Selectors for Objects

import { shallow } from 'zustand/shallow';

// Good: Shallow comparison prevents unnecessary re-renders
const { name, email } = useStore(
  (state) => ({ name: state.name, email: state.email }),
  shallow
);

Summary

FeatureUsage
createCreate store
setUpdate state
getAccess state in actions
persistSave to storage
devtoolsRedux DevTools
immerImmutable updates
shallowPrevent re-renders

Zustand provides a simple yet powerful solution for React state management with minimal boilerplate.

Is your AI API down right now? Check live status for OpenAI, Claude, Gemini, Groq, and more.
Check status →
MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.