Zustand

A small, fast, and scalable state management solution with minimal boilerplate

Zustand - Bear Necessities for State

Zustand (German for "state") is a small, fast, and scalable state management solution. It has a simple API based on hooks, doesn't require providers, and works with React's concurrent mode. Zustand is perfect for those who want the power of Redux without the boilerplate.

Why Zustand?

  • Minimal — Tiny bundle size (~1KB)
  • No Providers — No wrapper components needed
  • Hooks-based — Simple hook API
  • TypeScript — First-class TypeScript support
  • Flexible — Works outside React too

Installation

npm install zustand

Basic Store

import { create } from 'zustand';

// Create a store with state and actions
const useStore = create((set) => ({
  // State
  count: 0,
  
  // Actions
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  setCount: (value) => set({ count: value }),
}));

// Use in components - no Provider needed!
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  const decrement = useStore((state) => state.decrement);
  
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

// Or destructure multiple values (causes re-render when any changes)
function CounterAlt() {
  const { count, increment, decrement } = useStore();
  return (/* same JSX */);
}

Todo App Example

import { create } from 'zustand';

const useTodoStore = create((set, get) => ({
  todos: [],
  filter: 'all', // 'all' | 'active' | 'completed'
  
  // Actions
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, {
      id: Date.now(),
      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 }),
  
  // Computed values using get()
  getFilteredTodos: () => {
    const { todos, filter } = get();
    switch (filter) {
      case 'active': return todos.filter(t => !t.completed);
      case 'completed': return todos.filter(t => t.completed);
      default: return todos;
    }
  },
  
  clearCompleted: () => set((state) => ({
    todos: state.todos.filter(todo => !todo.completed),
  })),
}));

// Components
function TodoList() {
  const todos = useTodoStore((state) => state.getFilteredTodos());
  const toggleTodo = useTodoStore((state) => state.toggleTodo);
  const deleteTodo = useTodoStore((state) => state.deleteTodo);
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

Async Actions

const useUserStore = create((set, get) => ({
  users: [],
  loading: false,
  error: null,
  
  fetchUsers: async () => {
    set({ loading: true, error: null });
    
    try {
      const response = await fetch('/api/users');
      const users = await response.json();
      set({ users, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
  
  fetchUserById: async (id) => {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    
    // Merge with existing users
    set((state) => ({
      users: state.users.some(u => u.id === id)
        ? state.users.map(u => u.id === id ? user : u)
        : [...state.users, user]
    }));
    
    return user;
  },
}));

// Usage
function UsersList() {
  const { users, loading, error, fetchUsers } = useUserStore();
  
  useEffect(() => {
    fetchUsers();
  }, []);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Middleware - Persist, DevTools, Immer

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

// Persist to localStorage
const useStore = create(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'app-settings', // localStorage key
    }
  )
);

// DevTools integration
const useDebugStore = create(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
    }),
    { name: 'MyStore' }
  )
);

// Immer for immutable updates
const useTodoStore = create(
  immer((set) => ({
    todos: [],
    addTodo: (text) => set((state) => {
      // Can "mutate" with Immer!
      state.todos.push({ id: Date.now(), text, completed: false });
    }),
    toggleTodo: (id) => set((state) => {
      const todo = state.todos.find(t => t.id === id);
      if (todo) todo.completed = !todo.completed;
    }),
  }))
);

// Combine multiple middleware
const useAdvancedStore = create(
  devtools(
    persist(
      immer((set) => ({
        // ... state and actions
      })),
      { name: 'advanced-store' }
    )
  )
);

TypeScript Support

import { create } from 'zustand';

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

interface TodoState {
  todos: Todo[];
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  deleteTodo: (id: number) => void;
}

const useTodoStore = create<TodoState>()((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), 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),
  })),
}));

Zustand vs Redux

Feature Zustand Redux
Bundle size ~1KB ~10KB+
Provider Not needed Required
Boilerplate Minimal More verbose
Learning curve Low Higher
DevTools Via middleware Built-in

💡 Best Practices

  • • Use selector functions to prevent unnecessary re-renders
  • • Split stores by domain/feature for large apps
  • • Use persist middleware for data that should survive refreshes
  • • Enable devtools in development for debugging
  • • Use immer middleware for complex nested state updates