Skip to content

Zustand (Pinia vibes, less ceremony) ​

I've decided Zustand is the store I'd reach for first in a React SPA. Feels closer to Pinia than Redux ever did.

Install ​

bash
npm install zustand

A tiny store ​

ts
// stores/useCartStore.ts
import { create } from 'zustand'

type CartItem = { id: string; name: string; qty: number }

type CartState = {
  items: CartItem[]
  add: (item: CartItem) => void
  remove: (id: string) => void
  clear: () => void
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  add: (item) =>
    set((state) => ({ items: [...state.items, item] })),
  remove: (id) =>
    set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
  clear: () => set({ items: [] }),
}))

Comments for the code above:

  • One create() call - store + actions together (Pinia defineStore energy)
  • set merges / replaces - keep updates immutable
  • No Provider required for the basic case (nice)

Using it in a component ​

tsx
function CartBadge() {
  const items = useCartStore((s) => s.items)
  return <span>{items.length}</span>
}

function AddButton() {
  const add = useCartStore((s) => s.add)
  return (
    <button
      type="button"
      onClick={() => add({ id: '1', name: 'Sticker', qty: 1 })}
    >
      Add
    </button>
  )
}

As you can see - you pass a selector. Component re-renders when the selected slice changes. Select narrowly (s.items.length vs whole s) to avoid extra renders.

Pinia → Zustand map ​

PiniaZustand
defineStore('id', { state, actions, getters })create((set, get) => ({ … }))
storeToRefs / computed gettersselect in the hook, or derive outside
pluginsmiddleware (persist, devtools)
always works inside setupworks in components via the hook; getState() outside React too

Persist middleware (localStorage) ​

ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useSettingsStore = create(
  persist(
    (set) => ({
      density: 'cozy' as 'cozy' | 'compact',
      setDensity: (density: 'cozy' | 'compact') => set({ density }),
    }),
    { name: 'settings' },
  ),
)

What I put in Zustand vs Query ​

  • UI flags, wizard steps, client-only preferences → Zustand
  • API lists / details / mutations with cache → TanStack Query
  • Auth token sometimes lives in Zustand (or httpOnly cookies + server) - depends on the app

Next: TanStack Query - the library that made me stop writing fetch-in-Pinia clones.

Enjoy!