Learning Zustand - Core Concepts & Key Architecture
Episode 2 of 23

Learning Zustand - Core Concepts & Key Architecture

This episode breaks down the Zustand store model that holds state and actions in a single object, the subscribe/notify mechanism behind the scenes, the three core APIs useStore, setState, and subscribe, as well as the concepts of selectors, transient updates, and the vanilla store.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Now that you understand the history, it's time to break down Zustand's engine. Episode 2 explains the store model, how subscribe/notify works behind the scenes, and the three core APIs you will use in every upcoming episode. You don't need to master its internal implementation perfectly, but understanding this mental model will save you from confusing bugs.

We start with the simple store model, then move into the publish-subscribe mechanism that makes selective re-renders possible, and close with the three core APIs plus two key concepts: selectors and transient updates.

The Zustand Store Model

One Store Holding State and Actions

Zustand uses a single store object that holds two things at once: state and action. There is no reducer file separation and no separate action types. You define both in one create() call:

JSSimple store with state and actions
import { create } from 'zustand'
 
type BearState = {
  bears: number
  addBear: () => void
}
 
export const useBearStore = create<BearState>((set) => ({
  bears: 0,
  addBear: () => set((s) => ({ bears: s.bears + 1 })),
}))

Notice create((set) => ...): the set argument is the function used to update state, and the object returned becomes the store's content. State and actions live side by side in a single object — this is what makes Zustand so concise.

A Hook, Not a Provider

What most distinguishes Zustand from Context and Redux: create() returns a hook, not a Provider component. You call it directly in your component:

JSUsing the store as a hook
function BearCounter() {
  const bears = useBearStore((s) => s.bears)
  return <h1>{bears} bear</h1>
}

Because useBearStore is a hook, there is no <Provider> wrapping your application. useBearStore((s) => s.bears) is how you read state with a selector — the component subscribes to a specific slice, not the entire store.

How It Works Behind the Scenes

The Publish/Subscribe Pattern

Zustand is a subscribe/notify engine: the store keeps a list of listeners, and when state changes, the store notifies all listeners whose selector value also changed. This is the same pattern as an event emitter, where components act as subscribers.

State Updates and Notifications

Each set() produces new state. Zustand compares each listener's selector result with strict equality (===). Only listeners whose selector result differs get notified — this is where selective re-rendering comes from.

JSsetState and getState outside hooks
useBearStore.setState({ bears: 5 })
const current = useBearStore.getState()

useBearStore.setState({ bears: 5 }) updates the state and triggers notifications; useBearStore.getState() reads the current state without subscribing. Both APIs can be used outside components — for example, from event handlers or non-React logic.

Selectors and Strict Equality

A selector is a function that takes the full state and returns the part you need. Zustand stores the last selector result; if the result is exactly the same by reference, the component is not re-rendered. That's why selectors that return a new object every time are a trap — we'll discuss this in episodes 5 and 13.

The Three Core APIs

useStore(selector)

The hook for reading state inside React with automatic subscription. Its full form is useStore(store, selector), and the store produced by create() is already bound, so using useBearStore(selector) is enough.

setState and getState

setState(partial) accepts a partial object or an updater function; getState() reads the entire state at that moment. Both are available on the hook store and the vanilla store.

subscribe(listener)

subscribe(listener) registers a listener that is called whenever state changes:

JSsubscribe outside hooks
const unsub = useBearStore.subscribe((state, prevState) => {
  if (state.bears !== prevState.bears) {
    console.log('bears berubah!')
  }
})

useBearStore.subscribe((state, prevState) => ...) accepts a listener with the new state and the old state, then returns an unsub function to stop subscribing. For selector-based subscriptions, you need the subscribeWithSelector middleware — a topic for episode 5.

Key Concepts

Transient Updates

There are times when state changes very often — for example, mouse position or download progress — and not every change deserves to trigger a render. Zustand supports transient updates: updating via getState and setState outside React, or manually subscribing to a ref without re-rendering. This pattern is optimized in episode 13.

Vanilla Store

Zustand's core doesn't depend on React. createStore from zustand/vanilla produces the same pure store, minus the hook:

JSVanilla store without React
import { createStore } from 'zustand/vanilla'
 
const store = createStore((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}))
 
store.getState().inc()

createStore((set) => ...) produces an object with getState, setState, and subscribe that can be used in Node.js, workers, or event buses. In React, this store is connected via useStore — full details in episode 17.

Closing

Episode 2 gives you Zustand's core mental model: one store holding state and actions, created with create() which returns a hook without a Provider, built on the subscribe/notify pattern with strict equality, and exposing the three core APIs useStore, setState, and subscribe.

Key takeaways:

  • A Zustand store is a single object holding state and actions, created with create().
  • create() returns a hook, not a Provider.
  • Selective re-renders come from strict equality comparison of selector results.
  • setState updates state; getState reads state without subscribing.
  • subscribe(listener) registers a listener and returns an unsub function.
  • The zustand/vanilla core can be used without React at all.

In the next episode we start practicing: setup, installation, and your first store — creating a React project, installing zustand, organizing the stores folder structure, and writing a counter store used directly in a component. Get your terminal ready!