Skip to main content
Back to Blog
Tutorials
2 min read
December 17, 2024

How to Build a State Management System with Zustand in React

Learn to manage global state in React with Zustand — typed stores, slices, middleware, persistence, and devtools integration.

Ryel Banfield

Founder & Lead Developer

Zustand is a minimal, fast state manager for React. Here is how to use it effectively in real apps.

Install

pnpm add zustand

Basic Store

// stores/useCounterStore.ts
import { create } from "zustand";

interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

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

Async Actions and Complex State

// stores/useProductStore.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

interface Filters {
  search: string;
  category: string | null;
  minPrice: number;
  maxPrice: number;
}

interface ProductState {
  products: Product[];
  filters: Filters;
  loading: boolean;
  error: string | null;

  fetchProducts: () => Promise<void>;
  setFilter: <K extends keyof Filters>(key: K, value: Filters[K]) => void;
  resetFilters: () => void;
  filteredProducts: () => Product[];
}

const defaultFilters: Filters = {
  search: "",
  category: null,
  minPrice: 0,
  maxPrice: Infinity,
};

export const useProductStore = create<ProductState>()(
  devtools(
    persist(
      immer((set, get) => ({
        products: [],
        filters: { ...defaultFilters },
        loading: false,
        error: null,

        fetchProducts: async () => {
          set({ loading: true, error: null });
          try {
            const res = await fetch("/api/products");
            if (!res.ok) throw new Error("Failed to fetch products");
            const products = await res.json();
            set({ products, loading: false });
          } catch (err) {
            set({
              error: err instanceof Error ? err.message : "Unknown error",
              loading: false,
            });
          }
        },

        setFilter: (key, value) => {
          set((state) => {
            state.filters[key] = value;
          });
        },

        resetFilters: () => {
          set({ filters: { ...defaultFilters } });
        },

        filteredProducts: () => {
          const { products, filters } = get();
          return products.filter((p) => {
            if (filters.search && !p.name.toLowerCase().includes(filters.search.toLowerCase())) {
              return false;
            }
            if (filters.category && p.category !== filters.category) return false;
            if (p.price < filters.minPrice) return false;
            if (p.price > filters.maxPrice) return false;
            return true;
          });
        },
      })),
      { name: "product-store", partialize: (state) => ({ filters: state.filters }) }
    ),
    { name: "ProductStore" }
  )
);

Slice Pattern for Large Stores

// stores/slices/authSlice.ts
import type { StateCreator } from "zustand";
import type { AppState } from "../useAppStore";

export interface AuthSlice {
  user: { id: string; name: string; email: string } | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

export const createAuthSlice: StateCreator<AppState, [], [], AuthSlice> = (
  set
) => ({
  user: null,
  token: null,

  login: async (email, password) => {
    const res = await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });
    if (!res.ok) throw new Error("Login failed");
    const { user, token } = await res.json();
    set({ user, token });
  },

  logout: () => set({ user: null, token: null }),
});

// stores/slices/uiSlice.ts
export interface UISlice {
  sidebarOpen: boolean;
  theme: "light" | "dark" | "system";
  toggleSidebar: () => void;
  setTheme: (theme: UISlice["theme"]) => void;
}

export const createUISlice: StateCreator<AppState, [], [], UISlice> = (
  set
) => ({
  sidebarOpen: true,
  theme: "system",
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
  setTheme: (theme) => set({ theme }),
});

// stores/useAppStore.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { createAuthSlice, type AuthSlice } from "./slices/authSlice";
import { createUISlice, type UISlice } from "./slices/uiSlice";

export type AppState = AuthSlice & UISlice;

export const useAppStore = create<AppState>()(
  devtools((...a) => ({
    ...createAuthSlice(...a),
    ...createUISlice(...a),
  }))
);

Selectors for Performance

// Use selectors to prevent unnecessary re-renders
function UserName() {
  const name = useAppStore((s) => s.user?.name);
  return <span>{name ?? "Guest"}</span>;
}

// Create reusable selectors
const selectIsAuthenticated = (state: AppState) => state.user !== null;
const selectSidebarOpen = (state: AppState) => state.sidebarOpen;

function Sidebar() {
  const isOpen = useAppStore(selectSidebarOpen);
  if (!isOpen) return null;
  return <aside>...</aside>;
}

// Shallow comparison for object selectors
import { useShallow } from "zustand/react/shallow";

function UserCard() {
  const { name, email } = useAppStore(
    useShallow((s) => ({
      name: s.user?.name ?? "",
      email: s.user?.email ?? "",
    }))
  );
  return (
    <div>
      <p>{name}</p>
      <p>{email}</p>
    </div>
  );
}

Subscribe Outside React

// Listen for state changes outside components
const unsubscribe = useAppStore.subscribe(
  (state) => state.user,
  (user, prevUser) => {
    if (user && !prevUser) {
      console.log("User logged in:", user.email);
    }
    if (!user && prevUser) {
      console.log("User logged out");
    }
  }
);

Need Help Building Complex React Apps?

We design scalable frontend architectures for production applications. Get in touch to discuss your project.

Zustandstate managementReactTypeScripttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles