Lightweight State with Zustand

Zustand (German for "state") is a tiny global state library — about 1 KB — with almost no boilerplate. You call create() once to make a store, and any component can read it through the returned hook. No Provider, no actions, no dispatch. In this lesson you'll build a store, read it with selectors to avoid wasted re-renders, update it from anywhere, and learn when Zustand beats Context or Redux.

Learn Lightweight State with Zustand in our free React course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and a…

Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

1️⃣ Creating a Store

You define a store once, in its own module. create() takes a function that receives set (and optionally get ) and returns the initial state plus the functions that change it. It hands you back a hook — conventionally named useSomethingStore .

Notice set does a shallow merge — you only return the keys that changed. The demo below models the store so you can watch set and get work:

2️⃣ Selectors: Subscribe to Only What You Need

If you call useStore() with no argument, your component re-renders on every store change. Pass a selector and you subscribe to a single slice — the component re-renders only when that slice changes. This is the most important habit in Zustand.

Selectors are just pure functions of state — they can also derive values (a total, a count) without storing them. The demo shows selecting and deriving from a cart store:

3️⃣ Your Turn: A Tasks Store

Time to wire up the actions yourself. Fill in the blanks so addTask appends without mutating, and clear empties the list. Remember: set with an updater receives current state and returns the changed keys.

4️⃣ Updating from Outside React

Because the store lives in a module, you can touch it without a component. The hook itself carries getState() and setState() . This is gold for websocket handlers, keyboard listeners, or any imperative code.

5️⃣ Persistence with Middleware

Zustand ships small middlewares you wrap around your initializer. persist saves the store to localStorage automatically, so state survives a page reload — perfect for a theme, a cart, or recent searches.

These lines of a Zustand useAuthStore are scrambled. Put them in the correct order:

No. The store lives in a module and the generated hook reads it directly. That's a key difference from Context and Redux.

2) Why pass a selector like useStore(s => s.count) instead of useStore() ?

So the component re-renders only when that specific slice changes, not on every store update. Narrow selection is the main performance habit.

3) How do you update the store from a plain websocket handler outside React?

Use useStore.getState() to call an action or useStore.setState(...) to write directly — both work anywhere.

📋 Quick Reference

Model a store with a likes count and a like() action. Call like() three times and log the result — the engine behind every "heart" button.

Practice quiz

Do you need a Provider component to use a Zustand store?

  • Yes, you must wrap your app in a Provider
  • Only for nested components
  • No — create() returns a hook you import anywhere; the store lives in a module
  • Only in production builds

Answer: No — create() returns a hook you import anywhere; the store lives in a module. Zustand needs no Provider. create() returns a hook backed by a module-level store, so any component can subscribe directly.

What does create() return?

  • A hook (conventionally useSomethingStore) backed by the store
  • A reducer function
  • A React context object
  • A plain state object

Answer: A hook (conventionally useSomethingStore) backed by the store. create((set) => ({ ... })) returns a hook you call to read the store, conventionally named useSomethingStore.

Why pass a selector like useStore(s => s.count) instead of useStore()?

  • It is required syntax
  • It makes the store global
  • To avoid needing a Provider
  • So the component re-renders only when that slice changes, not on every store update

Answer: So the component re-renders only when that slice changes, not on every store update. A selector subscribes the component to a single slice, so it re-renders only when that slice changes. Narrow selection is the main Zustand performance habit.

How does Zustand's set() update state by default?

  • It replaces the entire state object
  • It shallow-merges the returned keys into the existing state
  • It deep-merges nested objects
  • It mutates state in place

Answer: It shallow-merges the returned keys into the existing state. set does a shallow merge — you return only the keys that changed and Zustand merges them into the existing state.

How do you update the store from outside React (e.g. a websocket handler)?

  • Use useStore.getState() to call an action or useStore.setState() to write
  • Call the hook useStore() directly
  • You cannot update from outside React
  • Wrap the code in a Provider

Answer: Use useStore.getState() to call an action or useStore.setState() to write. The hook exposes getState() and setState() that work anywhere — in listeners, websocket handlers, or plain utilities — outside any component.

What is wrong with useStore(s => ({ a: s.a, b: s.b })) without useShallow?

  • Nothing, it is optimal
  • It only selects one field
  • It returns a new object reference every render, causing constant re-renders
  • It mutates the store

Answer: It returns a new object reference every render, causing constant re-renders. Returning a fresh object each render means the selector result is never reference-equal, so the component re-renders constantly. Wrap it in useShallow.

What is a selector in Zustand?

  • A middleware
  • A pure function of state that reads (and can derive) a slice
  • A React component
  • An action creator

Answer: A pure function of state that reads (and can derive) a slice. Selectors are pure functions of state, like s => s.items.length, that read a slice or derive a value without storing it.

Why does s.items.push(x) fail to update subscribers in Zustand?

  • push is not a function
  • Arrays cannot be stored
  • It needs a Provider
  • It mutates state in place instead of returning a new array via set

Answer: It mutates state in place instead of returning a new array via set. Mutating state directly does not notify subscribers. Return a new array/object from set, e.g. set(s => ({ items: [...s.items, x] })), or use the immer middleware.

What does the persist middleware do?

  • Logs every action to the console
  • Saves the store to localStorage so state survives a page reload
  • Adds undo/redo
  • Makes selectors faster

Answer: Saves the store to localStorage so state survives a page reload. persist wraps your initializer and automatically saves/restores the store to localStorage, so state like a theme or cart survives reloads.

How is Zustand different from Redux Toolkit?

  • Zustand requires more boilerplate
  • Zustand cannot be used in large apps
  • Zustand has far less ceremony — no actions, reducers, dispatch, or providers
  • Zustand only works with class components

Answer: Zustand has far less ceremony — no actions, reducers, dispatch, or providers. Both are global stores, but Zustand is minimalist: you write state plus the functions that mutate it and read with a hook — no actions, reducers, dispatch, or Provider.