DEV Community

Alex Spinov
Alex Spinov

Posted on

Zustand Has a Free State Manager: Simple React State Without Providers, Reducers, or Boilerplate

Redux needs actions, reducers, action creators, selectors, middleware, and a Provider wrapper. Context API re-renders every consumer when any value changes. You just want shared state that works.

What if global state was a single function call? No Provider. No reducer. No boilerplate.

That's Zustand. 1KB, no dependencies, works with React out of the box.

The Simplest Store

import { create } from "zustand";

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

const useCounter = 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 ANY component — no Provider needed
function Counter() {
  const count = useCounter((state) => state.count);
  const increment = useCounter((state) => state.increment);

  return <button onClick={increment}>Count: {count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

That's the entire setup. No <Provider>, no createStore(), no combineReducers().

Real-World Store: Shopping Cart

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

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, "quantity">) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  total: () => number;
  clearCart: () => void;
}

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

  addItem: (item) => set((state) => {
    const existing = state.items.find((i) => i.id === item.id);
    if (existing) {
      return {
        items: state.items.map((i) =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        ),
      };
    }
    return { items: [...state.items, { ...item, quantity: 1 }] };
  }),

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

  updateQuantity: (id, quantity) => set((state) => ({
    items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
  })),

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

  clearCart: () => set({ items: [] }),
}));
Enter fullscreen mode Exit fullscreen mode

Middleware: Persist, DevTools, Immer

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

const useStore = create<StoreState>()(
  devtools(
    persist(
      immer((set) => ({
        todos: [],
        addTodo: (text: string) => set((state) => {
          // Immer lets you "mutate" — it creates immutable updates
          state.todos.push({ id: crypto.randomUUID(), text, done: false });
        }),
        toggleTodo: (id: string) => set((state) => {
          const todo = state.todos.find((t) => t.id === id);
          if (todo) todo.done = !todo.done;
        }),
      })),
      { name: "todo-storage" } // Persists to localStorage
    ),
    { name: "TodoStore" } // Shows in Redux DevTools
  )
);
Enter fullscreen mode Exit fullscreen mode

Selective Rendering — Only Re-Render What Changes

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

// ✅ Only re-renders when user.name changes
const name = useStore((state) => state.user.name);

// ❌ Re-renders on ANY state change (avoid this)
const everything = useStore();
Enter fullscreen mode Exit fullscreen mode

Zustand vs Redux vs Context vs Jotai

Feature Zustand Redux Toolkit Context API Jotai
Bundle size 1 KB 11 KB 0 (built-in) 2 KB
Boilerplate Minimal Moderate Low Minimal
Provider needed No Yes Yes Yes
DevTools Via middleware Built-in No Via plugin
Persistence Via middleware Manual Manual Via plugin
Re-render control Selector-based Selector-based Poor Atom-based
Learning curve 5 minutes 30 minutes 10 minutes 10 minutes

When to Choose Zustand

Choose Zustand when:

  • You want the simplest possible global state
  • No Provider wrapper is a priority (micro-frontends, libraries)
  • You need selectors to prevent unnecessary re-renders
  • Your state logic is straightforward (not deeply nested atoms)

Skip Zustand when:

  • You need atomic state (individual pieces of state) → Jotai
  • You need extensive middleware ecosystem → Redux Toolkit
  • Your state is component-local → just use useState

The Bottom Line

Zustand is state management reduced to its essence. Create a store, use it in components, done. No ceremony, no boilerplate, no architecture debates.

Start here: zustand.docs.pmnd.rs


Need custom data extraction, scraping, or automation? I build tools that collect and process data at scale — 78 actors on Apify Store and 265+ open-source repos. Email me: Spinov001@gmail.com | My Apify Actors

Top comments (0)