Zustand

Simple and fast state management for React

🐻 What is Zustand?

Zustand is a small, fast state management solution for React. It uses hooks and requires minimal boilerplate. Unlike Redux, Zustand is simpler to set up and doesn't need providers or complex configuration.


import create from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}));
                                    

Result:

A simple store with state and actions in just a few lines!

Zustand Key Features

Simple API

Minimal boilerplate code

const useStore = create((set) => ({
  count: 0
}));
🎣

Hook-Based

Use stores with React hooks

const count = useStore(
  (state) => state.count
);
🚫

No Providers

No context providers needed

// Just use the hook anywhere
const data = useStore();
📦

Tiny Size

Only ~1KB gzipped

npm install zustand

🔹 Creating Your First Store

Create a Zustand store with the create function. Define your state and actions in a single object for simple, organized state management.

# Install Zustand
npm install zustand
// store.js
import create from 'zustand';

const useStore = create((set) => ({
  // State
  count: 0,
  user: null,
  
  // Actions
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  setUser: (user) => set({ user })
}));

export default useStore;
// Counter.jsx
import React from 'react';
import useStore from './store';

function Counter() {
  // Select specific state
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  const decrement = useStore((state) => state.decrement);
  const reset = useStore((state) => state.reset);

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Output:

Counter works without any providers or complex setup!

🔹 Selecting State Efficiently

Select only the state you need to prevent unnecessary re-renders. Zustand automatically optimizes updates when you select specific state slices.

import React from 'react';
import useStore from './store';

function UserProfile() {
  // ✅ Good: Select only what you need
  const user = useStore((state) => state.user);
  
  // ❌ Bad: Selects entire store (re-renders on any change)
  // const { user } = useStore();

  return (
    <div>
      {user ? (
        <p>Welcome, {user.name}!</p>
      ) : (
        <p>Please log in</p>
      )}
    </div>
  );
}

function CountDisplay() {
  // Only re-renders when count changes
  const count = useStore((state) => state.count);
  
  console.log('CountDisplay rendered');
  
  return <div>Count: {count}</div>;
}

function UserDisplay() {
  // Only re-renders when user changes
  const user = useStore((state) => state.user);
  
  console.log('UserDisplay rendered');
  
  return <div>User: {user?.name || 'None'}</div>;
}

Output:

Components only re-render when their selected state changes.

🔹 Async Actions

Handle async operations like API calls directly in your store actions. Zustand makes async state management straightforward without extra middleware.

// store.js
import create from 'zustand';

const useStore = create((set) => ({
  users: [],
  loading: false,
  error: null,
  
  // Async action to fetch users
  fetchUsers: async () => {
    set({ loading: true, error: null });
    
    try {
      const response = await fetch('/api/users');
      const data = await response.json();
      set({ users: data, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
  
  // Async action to add user
  addUser: async (userData) => {
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      const newUser = await response.json();
      
      set((state) => ({
        users: [...state.users, newUser]
      }));
    } catch (error) {
      set({ error: error.message });
    }
  }
}));

export default useStore;
// UserList.jsx
import React, { useEffect } from 'react';
import useStore from './store';

function UserList() {
  const users = useStore((state) => state.users);
  const loading = useStore((state) => state.loading);
  const error = useStore((state) => state.error);
  const fetchUsers = useStore((state) => state.fetchUsers);

  useEffect(() => {
    fetchUsers();
  }, [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>
  );
}

Output:

Shows loading state, then displays users or error message.

🔹 Multiple Stores

Create separate stores for different features to keep your code organized. Each store is independent and can be used anywhere in your app.

// authStore.js
import create from 'zustand';

export const useAuthStore = create((set) => ({
  user: null,
  isAuthenticated: false,
  
  login: (user) => set({ user, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false })
}));
// todoStore.js
import create from 'zustand';

export const useTodoStore = create((set) => ({
  todos: [],
  
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, done: false }]
  })),
  
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    )
  })),
  
  deleteTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  }))
}));
// App.jsx
import React from 'react';
import { useAuthStore } from './authStore';
import { useTodoStore } from './todoStore';

function App() {
  const user = useAuthStore((state) => state.user);
  const todos = useTodoStore((state) => state.todos);
  
  return (
    <div>
      <h1>Welcome {user?.name || 'Guest'}</h1>
      <p>You have {todos.length} todos</p>
    </div>
  );
}

Output:

Multiple independent stores work together seamlessly.

🔹 Persisting State

Persist store data to localStorage automatically with Zustand's persist middleware. State survives page refreshes without extra code.

// store.js
import create from 'zustand';
import { persist } from 'zustand/middleware';

const useStore = create(
  persist(
    (set) => ({
      count: 0,
      user: null,
      
      increment: () => set((state) => ({ count: state.count + 1 })),
      setUser: (user) => set({ user })
    }),
    {
      name: 'my-app-storage', // localStorage key
      // Optional: customize what to persist
      partialize: (state) => ({
        count: state.count,
        user: state.user
      })
    }
  )
);

export default useStore;
// Counter.jsx
import React from 'react';
import useStore from './store';

function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>Increment</button>
      <p>Refresh the page - count persists!</p>
    </div>
  );
}

Output:

Count value persists across page refreshes automatically!

🔹 Zustand vs Redux

Zustand and Redux both manage state, but Zustand is simpler and requires less code. Choose based on your project needs.

Zustand Advantages:

  • ✅ Much less boilerplate code
  • ✅ No providers needed
  • ✅ Smaller bundle size (~1KB)
  • ✅ Simpler learning curve
  • ✅ Built-in async support

Redux Advantages:

  • ✅ Larger ecosystem and community
  • ✅ More middleware options
  • ✅ Better DevTools integration
  • ✅ Established patterns for large apps
// Zustand - Simple and concise
const useStore = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 }))
}));

// Redux Toolkit - More structure
const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1; }
  }
});

🧠 Test Your Knowledge

Does Zustand require a Provider component?