# Zustand

## 1. A Minimal, Hook-Based Store — No Provider Required

Zustand is a small state-management library built around one core idea: a store is just a **hook** you call. There's no `<Provider>` wrapping your app, no action-type constants, no reducer switch statements — you define a store with `create()`, and any component imports and calls the resulting hook directly, exactly like `useState`, except the state lives outside React and is shared across every component that calls the hook.

```tsx
import { create } from "zustand";

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

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

function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}
```

Contrast with Redux (previous two topics): no store needs to be threaded through a `<Provider>` at the app root, no `dispatch({ type: ... })` ceremony — `increment` is a plain function living right next to the state it updates, called directly.

## 2. `set`: Direct State Updates, No Action Objects

```ts
const useStore = create<{ user: string | null; setUser: (name: string) => void; logout: () => void }>(
  (set, get) => ({
    user: null,
    setUser: (name) => set({ user: name }),        // shallow-merges { user: name } into state
    logout: () => set({ user: null }),
    greet: () => `Hello, ${get().user}`,             // get() reads current state outside a component
  })
);
```

`set(partialState)` shallow-merges the given object into the existing state (like `this.setState` in old-style class components) — you only specify the keys that changed, everything else is preserved automatically. `set((state) => partialState)` (the functional form, used in section 1's `increment`) is used whenever the update depends on the current value, avoiding stale-closure bugs the same way `useState`'s updater-function form does. `get()` reads the current state from inside a store action, useful when one action's logic needs to read other pieces of state without waiting for a re-render.

## 3. Selective Subscriptions: the Real Point of the Hook Argument

```tsx
// Selects ONLY `count` — this component re-renders ONLY when count changes,
// never when unrelated store fields (like `user` in a combined store) change
const count = useCounterStore((state) => state.count);

// Selecting the WHOLE state object (no selector) re-renders on ANY change —
// almost always wrong for a store with more than one independent field
const wholeState = useCounterStore(); // avoid this pattern for multi-field stores
```

Every `useStore(selector)` call subscribes that component to exactly the slice `selector` returns, using reference/shallow equality to decide whether to re-render — passing no selector (or an identity selector returning the whole state) subscribes to *every* change, defeating the fine-grained re-render control that's the actual reason to pass a selector function at all. This is the same underlying concern as Redux's `useSelector` from the previous topics, expressed through a plain function argument instead of a separate hook.

## 4. Multiple Independent Stores, or One Combined Store

```ts
// Option A: separate stores per concern — each is its own hook
const useAuthStore = create<AuthState>(...);
const useCartStore = create<CartState>(...);

// Option B: one store, multiple logical "slices" combined manually
const useAppStore = create<AuthState & CartState>((set, get) => ({
  ...createAuthSlice(set, get),
  ...createCartSlice(set, get),
}));
```

Unlike Redux, which pushes toward one store with `combineReducers`-style slices, Zustand has no strong opinion here — many small independent stores (Option A) are just as idiomatic as one larger combined store (Option B), since there's no single-Provider constraint forcing consolidation. Teams pick based on how often different pieces of state actually change together.

## 5. Middleware: `persist`, `devtools`, and Friends

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

const useSettingsStore = create<SettingsState>()(
  devtools(
    persist(
      (set) => ({
        theme: "light",
        setTheme: (theme) => set({ theme }),
      }),
      { name: "settings-storage" } // localStorage key
    )
  )
);
```

Zustand supports optional middleware wrapping the store creator function — `persist` automatically syncs store state to `localStorage` (or another storage adapter) and rehydrates it on load, `devtools` wires the store into Redux DevTools for the same time-travel debugging experience covered in the Redux topics, applied here to a store with no Redux underneath it at all.

## Under the Hood

**Zustand's store is a plain external object with a subscribe/publish mechanism — React's involvement is limited to one small hook wrapping that subscription.** `create(initializer)` builds a store object (`{ getState, setState, subscribe }`, conceptually similar to the plain-Redux store from the previous topics but without any reducer/action-type machinery) that lives entirely outside React's render tree, in a plain JS closure. The `useStore` hook it returns internally calls `useSyncExternalStore` (React's built-in primitive for subscribing a component to state that lives outside React, also what `react-redux`'s modern `useSelector` is built on) with your selector function — React re-renders the component if the selector's result changes according to a comparison function (`Object.is` by default), and does nothing otherwise. There's no Context, no Provider tree walk, no reconciliation-triggered re-computation; it's a direct subscription from the component straight to the external store object.

**`set`'s shallow merge is a plain `Object.assign`-style spread, not Immer-powered draft mutation.** Unlike Redux Toolkit's `createSlice` (previous topic), calling `set({ count: 5 })` does not track deep mutations through a Proxy — it computes `{ ...currentState, ...partialState }` (or, for the functional form, `{ ...currentState, ...updaterFn(currentState) }`) and republishes that as the new state, notifying every subscribed selector to re-check. This is exactly why nested object updates in Zustand still require the same explicit-spread discipline that plain Redux reducers needed before Immer — `set({ user: { ...state.user, name: "New" } })`, not `set((state) => { state.user.name = "New"; })`, unless you separately opt into Zustand's own Immer-integration middleware.

![A Zustand store lives outside React; each hook subscribes with a selector and re-renders only when its selected slice changes, while a selector-less hook re-renders on every change](diagrams/01-selector-subscriptions.svg)

## Common Pitfalls

1. **Selecting the whole store state with no selector function** (`useStore()` instead of `useStore((s) => s.count)`) — subscribes the component to every field's changes, causing unnecessary re-renders exactly like skipping a Redux `useSelector`'s narrowing.
2. **Mutating nested state directly inside `set`'s updater** without spreading, expecting Immer-like safety — plain Zustand does a shallow merge only; deep nested updates still need explicit spreading (or the separate Immer middleware).
3. **Creating a new store inside a component body** (`const useStore = create(...)` written directly inside a component function) instead of at module scope — creates a brand-new, disconnected store on every render instead of one shared store all components subscribe to.
4. **Returning a brand-new object/array from a selector on every call** (`useStore((s) => ({ a: s.a, b: s.b }))`) without a shallow-equality comparator — the default `Object.is` check sees a new object reference every time and re-renders on every store change regardless of whether `a`/`b` actually changed; Zustand exports a `shallow` comparator for exactly this case.
5. **Assuming `persist` middleware synchronizes across browser tabs automatically** — by default it only writes to `localStorage` and rehydrates on load; live cross-tab sync requires additional wiring (listening for `storage` events), not something `persist` does on its own.

## Quick Reference

```
create<T>((set, get) => ({ ...state, ...actions }))   → returns a useStore hook, no Provider needed

set(partialState)               → shallow-merges into state
set((state) => partialState)    → functional form, avoids stale-closure reads
get()                            → reads current state from inside a store action

useStore((state) => state.slice)  → selective subscription, re-renders only when that slice changes
useStore()                         → subscribes to EVERYTHING — usually wrong for multi-field stores

persist(initializer, { name })    → middleware: syncs state to localStorage, rehydrates on load
devtools(initializer)              → middleware: wires into Redux DevTools

Mechanism: plain external object + useSyncExternalStore, no Context/Provider tree involved
set() does a shallow merge (Object.assign-style) — NOT Immer draft mutation, unlike RTK's createSlice

Contrast: Redux/RTK = one store, actions/reducers, Provider-based, strong single-store convention
          Zustand    = any number of stores, direct functions, no Provider, lighter ceremony
```
