🐻Zustand

Create a store

Your store is a hook. State must be updated immutably and set merges state by default.

import { create } from "zustand";

interface BearState {
  bears: number;
  increasePopulation: () => void;
  removeAllBears: () => void;
}

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

Use in components

No providers needed. Select state and the component re-renders on changes.

function BearCounter() {
  const bears = useBearStore((state) => state.bears);
  return <h1>{bears} around here...</h1>;
}

function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

Selecting multiple state slices

Use useShallow to prevent unnecessary rerenders when selecting multiple values.

import { useShallow } from "zustand/react/shallow";

// Object pick
const { nuts, honey } = useBearStore(
  useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
);

// Array pick
const [nuts, honey] = useBearStore(
  useShallow((state) => [state.nuts, state.honey]),
);

Async actions

Just call set when ready. Zustand doesn’t care if actions are async.

const useFishStore = create<FishState>()((set) => ({
  fishies: {},
  fetch: async (pond: string) => {
    const response = await fetch(pond);
    set({ fishies: await response.json() });
  },
}));

Reading state in actions

Use get to access state outside of set.

const useSoundStore = create<SoundState>()((set, get) => ({
  sound: "grunt",
  action: () => {
    const sound = get().sound;
    // ...
  },
}));

Access state outside components

// Get non-reactive fresh state
const paw = useDogStore.getState().paw;

// Update state
useDogStore.setState({ paw: false });

// Subscribe to all changes
const unsub = useDogStore.subscribe(console.log);
unsub(); // unsubscribe

Overwriting state

The second argument to set replaces state instead of merging (default false).

const useFishStore = create<FishState>()((set) => ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () => set({}, true), // clears entire store
}));

Persist middleware

Persist store data to localStorage or other storage.

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

const useFishStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: "fish-storage", // unique name in storage
      storage: createJSONStorage(() => sessionStorage), // default: localStorage
    },
  ),
);

Immer middleware

Use Immer for mutable state updates.

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

const useBeeStore = create(
  immer((set) => ({
    bees: 0,
    addBees: (by: number) =>
      set((state) => {
        state.bees += by;
      }),
  })),
);

Redux DevTools

import { devtools } from "zustand/middleware";

const useBearStore = create(
  devtools(
    (set) => ({
      bears: 0,
      increase: (by: number) =>
        set(
          (state) => ({ bears: state.bears + by }),
          undefined,
          "bear/increase", // action name for devtools
        ),
    }),
    { name: "BearStore" },
  ),
);

Combining middleware

import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import type {} from "@redux-devtools/extension"; // required for devtools typing

interface BearState {
  bears: number;
  increase: (by: number) => void;
}

const useBearStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        increase: (by) => set((state) => ({ bears: state.bears + by })),
      }),
      { name: "bear-storage" },
    ),
  ),
);

Subscribe with selector

Use subscribeWithSelector middleware to subscribe to specific state changes.

import { subscribeWithSelector } from "zustand/middleware";

const useDogStore = create(
  subscribeWithSelector(() => ({ paw: true, snout: true })),
);

// Subscribe to specific state changes
const unsub = useDogStore.subscribe(
  (state) => state.paw,
  (paw, previousPaw) => console.log(paw, previousPaw),
);

Vanilla store (without React)

import { createStore } from "zustand/vanilla";

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

const { getState, setState, subscribe } = store;

Use with React via useStore:

import { useStore } from "zustand";
import { vanillaStore } from "./vanillaStore";

const useBoundStore = (selector) => useStore(vanillaStore, selector);

Transient updates

Subscribe without causing re-renders for performance-critical updates.

const Component = () => {
  const scratchRef = useRef(useScratchStore.getState().scratches);

  useEffect(
    () =>
      useScratchStore.subscribe((state) => {
        scratchRef.current = state.scratches;
      }),
    [],
  );
  // ...
};

Context for dependency injection

Use vanilla store with React context when you need providers.

import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'

const store = createStore(...)
const StoreContext = createContext(store)

const App = () => (
  <StoreContext.Provider value={store}>
    <Component />
  </StoreContext.Provider>
)

const Component = () => {
  const store = useContext(StoreContext)
  const slice = useStore(store, (state) => state.value)
  // ...
}