David John Thammineni
React14 Apr 20222 min read

Zustand: State Management Without the Ceremony

Zustand: State Management Without the Ceremony

My state-management journey: Redux everywhere, then Context everywhere, and now — for client state that's genuinely global — mostly Zustand. It's 1KB, has no provider wrapper, and re-renders exactly the components that read what changed.

A store in four lines

import create from "zustand";

const useCartStore = create((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
  remove: (id) =>
    set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
}));

Use it anywhere — no <Provider>:

function CartBadge() {
  const count = useCartStore((s) => s.items.length);
  return <Badge>{count}</Badge>;
}

Selectors are the performance model

CartBadge subscribes to items.length — it re-renders only when the length changes, not when an item's quantity updates. This is the granularity Context can't offer (any value change re-renders all consumers) without splitting contexts endlessly. The rule: select the smallest slice you need, and pass shallow for object selections:

import shallow from "zustand/shallow";
const { add, remove } = useCartStore(
  (s) => ({ add: s.add, remove: s.remove }),
  shallow
);

It works outside React

Because the store isn't trapped in the component tree, non-React code can read and write it — analytics, WebSocket handlers, imperative bridges:

socket.on("price", (p) => useCartStore.getState().updatePrice(p));

This is quietly the killer feature for real apps.

Where it sits in the stack

My current defaults: server cache in React Query (it's not client state!), local component state in useState, cross-cutting client state — cart, UI preferences, wizard progress — in Zustand. Redux still makes sense for teams that want strict action logs and time-travel debugging on complex flows. But most apps' "global state" is two stores and a dozen actions, and Zustand prices that correctly.

Keep Reading