Hui
Dev

Zustand Source Code Exploration

Dive into the inner workings of Zustand, a minimalist state management library for React. Discover how it leverages the classic Observer Pattern to eliminate unnecessary re-renders, and explore how its binding layer seamlessly connects a framework-agnostic core to React's fiber render pipeline.

Ching HuiChing Hui

Most React developers have experienced this situation. You put shared state into Context. Everything works. Until one update suddenly causes half of your component tree to re-render.

While diving into how Context and useReducer can be optimized to prevent unnecessary re-renders, I was reminded of Zustand—a lightweight state management library in the same realm as Redux and Jotai.

Introduction - Zustand
How to use Zustand

Instead of subscribing to an entire context, every component subscribes only to the state slice it actually needs. That design decision completely changes the rendering behavior.

  1. Components can subscribe to a specific slice of the state, eliminating the notorious unnecessary re-render issues inherent in the React Context API.
  2. Furthermore, Zustand's store design is entirely framework-agnostic; it doesn't inherently depend on React or any other framework.

It is elegantly simple yet powerful enough to manage shared state across numerous components. But how does it actually work?

How to use Zustand in React?

import { create } from 'zustand'

const useCountStore = create((set) => ({
  count: 0,
  incr: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
}))

The useCountStore constant holds the hook returned by the create function.

In a React component:

function Counter() {
  const count = useCountStore((state) => state.count)
  const incr = useCountStore((state) => state.incr)

  return <button onClick={incr}>count is {count}</button>
}

function ResetButton() {
  const reset = useCountStore((state) => state.reset)
  return <button onClick={reset}>reset</button>
}
  • Notice that there is no Provider. Both Counter and ResetButton access the exact same state without being wrapped in a Context Provider.
  • Selectors determine the re-render scope. The Counter component only subscribes to count, while ResetButton only subscribes to the reset action, preventing unnecessary updates.
  • Actions live right alongside the state, incr, reset and state live together. We don't need to write the dispatch or reducer.

Zustand's source code

  • vanilla.ts —— The core store itself, which is completely unaware of React's existence.
  • react.ts —— A binding layer that connects the vanilla store to React.

vanilla.ts: The Core Store

At its core, Zustand relies on a classic Observer Pattern.

There is an "observable entity" holding the state. External listeners can "subscribe" to it, and whenever the state updates, the entity "notifies" all subscribers.

More detail about Observer Pattern.

Learn more

Before looking at Zustand's implementation, let's temporarily forget React. Imagine we want to build the smallest possible state container ourselves.

We only need three things:

  1. somewhere to store state
  2. a way to subscribe
  3. a way to notify everyone after updates

That's exactly the Observer Pattern. A simplified implementation looks like this:

function createObservable(initial) {
  let state = initial
  const listeners = new Set()

  return {
    getState: () => state,
    setState: (next) => {
      state = next
      listeners.forEach((listener) => listener(state)) // notify all listeners
    },
    subscribe: (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener) // return unsubscribe
    },
  }
}

Zustand's createStoreImpl is essentially built upon this exact skeleton, with just a few more robust details added.

So .. What is inside of createStoreImpl?

zustand/src/vanilla.ts at main · pmndrs/zustand
🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.
const createStoreImpl = (createState) => {
  let state
  const listeners = new Set()

  const setState = (partial, replace) => {
    const nextState =
      typeof partial === 'function' ? partial(state) : partial

    if (!Object.is(nextState, state)) {   // ① bail-out
      const previousState = state
      state =
        (replace ?? (typeof nextState !== 'object' || nextState === null))
          ? nextState
          : Object.assign({}, state, nextState)    // ② shallow merge (default)
      
      // ③ Notify
      listeners.forEach((listener) => listener(state, previousState)) 
      
    }
  }

  const getState = () => state
  const getInitialState = () => initialState
  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }

  const api = { setState, getState, getInitialState, subscribe }
  const initialState = (state = createState(setState, getState, api))
  return api
}

export const createStore = (createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl

The entire store is encapsulated within a single closure.

Both state and listeners are private variables enclosed inside this scope. If external code wants to interact with them, the only way is through the exposed methods in the returned api object.

At first glance, setState looks like a normal setter. But it actually performs three independent responsibilities.

  1. Determine whether anything actually changed.
    • Bail-out via Object.is: If the newly computed nextState shares the exact same reference as the current state, the entire block is skipped, and listeners are not notified at all.
  1. Produce the next state.
    • Top-level Shallow Merge by Default: By default, Zustand performs a shallow merge. It uses Object.assign to merge your provided partial state into the old state, preserving all other untouched keys. Note that nested objects are not deeply merged.
    • If you intend to completely replace the entire state object instead of merging, you can pass `true` as the second argument: setState(nextState, true).
  1. Notify every subscriber: Only after passing the first two stages (the reference has changed and the merge is complete) will it trigger listeners.forEach(...), passing both the new and previous states (state, previousState) to every subscriber.

Even though this is pure Vanilla JS without any framework magic, it is inherently reactive.

import { createStore } from 'zustand/vanilla'

const store = createStore((set) => ({
  count: 0,
  incr: () => set((s) => ({ count: s.count + 1 })),
}))

// subscribe to a listener
const unsub = store.subscribe((state, prev) => {
  console.log(`count: ${prev.count} -> ${state.count}`)
})

store.getState().incr() // log: count: 0 -> 1
store.getState().incr() // log: count: 1 -> 2

unsub()                  // unscribe
store.getState().incr() // no more log

Let's dive into react.ts and connect this core skeleton to React!

            setState()





         vanilla store



         notify listeners





    useSyncExternalStore



         getSnapshot()



          selector(state)



          Object.is()

          ┌──────────┐
          │          │
      same         different
          │          │
       skip      re-render
zustand/src/react.ts at main · pmndrs/zustand
🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.
const createImpl = (createState) => {
  const api = createStore(createState) // ← store from vanilla.js

  const useBoundStore = (selector) => useStore(api, selector)

  Object.assign(useBoundStore, api) 
  return useBoundStore
}

export const create = (createState) =>
  createState ? createImpl(createState) : createImpl
  • The create function returns a hook that wraps a vanilla store initialized via createStore(createState) from 'vanilla.ts'.
  • Object.assign(useBoundStore, api) : This means getState, setState, subscribe, and getInitialState are all attached directly onto the returned hook itself. This brilliant API design explains why useCountStore can act as both a React hook inside components and a standalone object outside of them. You can seamlessly call useCountStore.getState() or useCountStore.setState() anywhere in your codebase—even completely outside of the React lifecycle.
// Inside a component
const count = useCountStore((state) => state.count) 

// Outside of React (e.g., in an API interceptor or a utility file)
const currentCount = useCountStore.getState().count

The useStore Implementation

export function useStore(api, selector = identity) {
  const slice = React.useSyncExternalStore(
    api.subscribe,
   
   // getSnapshot
    React.useCallback(() => selector(api.getState()), [api, selector]),

   // getServerSnapshot
    React.useCallback(() => selector(api.getInitialState()), [api, selector]),
  )
  React.useDebugValue(slice)
  return slice
}
  • api.subscribe tells React how to listen for changes in the external store.
  • selector(api.getState()) tells React what the current state slice looks like.

Whenever the store notifies React that an update has occurred, React invokes getSnapshot again to fetch the latest slice. It then evaluates whether the component needs a re-render based on that slice.

useSyncExternalStore

useSyncExternalStore is a React Hook that lets you subscribe to an external store.
useSyncExternalStore – React
The library for web and native user interfaces
  • subscribe: Registers a callback function. Whenever the store's state changes, this callback is fired, signaling to React that it's time to check for updates.
  • getSnapshot: Returns the current value extracted from the store. React calls this to grab the slice and uses Object.is behind the scenes to compare it with the previous snapshot. If they match, React bails out of the re-render.
  • getServerSnapshot: Tailored for Server-Side Rendering. This provides a consistent initial state during server-side rendering and client-side hydration, preventing the notorious "hydration mismatch" error. In Zustand, this maps directly to api.getInitialState().

From React's perspective, an external store only needs to answer two questions.

  1. Tell me when something changes. → That's subscribe.
  2. Tell me what the current snapshot looks like. → That's getSnapshot.

Whenever a notification arrives, React simply asks for a new snapshot and compares it with the previous one using Object.is. If the snapshot hasn't changed, React skips the render.

That means Zustand isn't deciding whether your component re-renders.

React is.

Zustand merely provides the data.

useSyncExternalStore source code: function mountSyncExternalStore in https://github.com/react/react/blob/e92ecef2806692f34e9d535da77405eb28464625/packages/react-reconciler/src/ReactFiberHooks.js#L1635

Takeaways

  • vanilla.ts: Implements a pure Observer Pattern using a simple closure (state and a listeners Set). Its setState method perfectly orchestrates the bail-out logic, top-level shallow merging, and subscriber notifications. This core store remains entirely framework-agnostic and completely decoupled from React.
  • react.ts: Acts as a razor-thin binding layer. The create function elegantly wraps the vanilla store into a custom React hook while attaching the store APIs directly to it. Meanwhile, useStore leverages useSyncExternalStore to seamlessly plug this external state right into React's fiber render pipeline.


The true brilliance of Zustand lies in its radical minimalism. It strips down the core logic of "how state is managed and notified" to its absolute bare essentials, while elegantly handing over the responsibility of "how React safely subscribes and schedules re-renders" to React's own native primitives.