While studying closures and scope, I realized Zustand is a great real-world example of how closures are leveraged. So I decided to dive into the source code myself.
Core Structure - vanilla.ts
The essence of Zustand is the ~100 lines of code in vanilla.ts. The rest is mostly “wrapping” for React and middleware integration.
For readability, let’s look at the code with types removed.
// vanilla.ts
const createStoreImpl = (createState) => {
// --- start of closure ---
let state;
const listeners = new Set();
const getState = () => state;
const setState = (partial, replace) => {
// partial can be a function or an object
const nextState = typeof partial === 'function' ? partial(state) : partial;
// update and notify only if previous and next states differ
if (!Object.is(nextState, state)) {
const previousState = state;
// replace with the new state (merge objects, overwrite otherwise)
state =
replace ?? (typeof nextState !== 'object' || nextState === null)
? nextState
: Object.assign({}, state, nextState);
// notify all subscribers (listeners)
listeners.forEach((listener) => listener(state, previousState));
}
};
const subscribe = (listener) => {
listeners.add(listener);
// return an unsubscribe function
return () => listeners.delete(listener);
};
const destroy = () => listeners.clear();
// API object exposed to the outside
const api = { setState, getState, subscribe, destroy };
// initialize! run createState to set the initial state
state = createState(setState, getState, api);
return api;
// --- end of closure ---
};
export const createStore = (createState) =>
createState ? createStoreImpl(createState) : createStoreImpl;When createStoreImpl runs, the variables state and listeners declared inside it do not disappear like ordinary local variables—thanks to the closure, they remain alive in memory.
The functions included in the returned api object—getState, setState, and subscribe—remember the environment (scope) at the time they were created. That scope includes state and listeners.
- Encapsulation: The
statevariable is “trapped” inside the scope ofcreateStoreImpl. From the outside, it can only be accessed through API methods likegetStateorsetState.- State Persistence: Although
createStoreImplis invoked only once, the resultingstateandlistenerspersist as long as the application runs, because the API methods keep referencing them.
React Integration - react.ts
Now let’s see how the vanilla store is integrated with React. Again, types are removed.
import React from 'react';
import { createStore } from './vanilla.js';
// a bridge hook that connects the vanilla store to React
export function useStore(api, selector) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState())
);
return slice;
}
const createImpl = (createState) => {
// 1. create the vanilla store API (state and listeners are created here via closure)
const api = createStore(createState);
// 2. create a dedicated hook bound to this store
const useBoundStore = (selector) => useStore(api, selector);
// 3. attach api methods (setState, etc.) onto the hook itself
Object.assign(useBoundStore, api);
return useBoundStore;
};
export const create = (createState) =>
createState ? createImpl(createState) : createImpl;- Create the vanilla store.
- Build a hook that returns
useSyncExternalStoreviauseBoundStore. - Bind API methods (
setState,getState, etc.) to the returned hook.
The role of useSyncExternalStore
useSyncExternalStore is a hook introduced in React 18 that synchronizes external stores with React. It tells React to subscribe to an external data source (the Zustand store here) and re-render when changes occur.
// inside useSyncExternalStore (simplified)
useEffect(() => {
if (checkIfSnapshotChanged(inst)) {
forceUpdate({inst});
}
const handleStoreChange = () => {
if (checkIfSnapshotChanged(inst)) {
// force re-render
forceUpdate({inst});
}
};
// subscribe to the store and return a cleanup function
return subscribe(handleStoreChange);
}, [subscribe]);In useSyncExternalStore, a function called handleStoreChange—which compares values and forces a component update—is registered as a listener. Through this mechanism, when setState invokes listeners, handleStoreChange runs and triggers a re-render of the component.
Inspecting in DevTools
const useBearStore = create((set, get) => ({
bears: 0,
increase: () => set(state => ({ bears: state.bears + 1 })),
}));
function Component() {
const bears = useBearStore(state => state.bears);
return <h1>{bears} bears</h1>;
}If you actually run a component like the above and check the Sources → Scope tab in DevTools, you can see everything captured by closures like this:

Step-by-step flow
// 1. Create the store hook
const useBearStore = create((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}));
// 2. Use the store in a component
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} bears</h1>;
}
// 3. Update state
function Controls() {
const increase = useBearStore((state) => state.increase);
return <button onClick={increase}>one up</button>;
}Step 1: Create store (create)
- Calling
createrunscreateStoreImpl. - At this time, a unique
stateandlistenersforuseBearStoreare created in the closure scope. stateis initialized as{ bears: 0, increase: [Function] }. Theincreasefunction rememberssetStatethrough the closure.- A custom hook named
useBearStoreis returned. This hook also carries methods likesetStateandgetState.
Step 2: Component render and subscription (useBearStore)
- When the
BearCountercomponent first renders, theuseBearStorehook runs. useSyncExternalStoreis called, and a re-rendering function is registered in thelistenerscreated in Step 1 viasubscribe.- The
getSnapshotcallback() => state.bearsruns, obtains the initial value0, assigns it tobears, and renders "0 bears" on the screen.
Step 3: State update (increase)
- When the user clicks the button, the
increasefunction obtained in theControlscomponent runs. increasecallssetState, which it remembers via closure.setStateupdatesstateto{ bears: 1 }.setStateiterates overlistenersand calls every registered function.- The 're-render function' registered by
useSyncExternalStoreinBearCounteris called. - React re-renders the
BearCountercomponent, runsgetSnapshotagain, obtains the new value1, and updates the screen.
Wrap-up
Zustand feels like a perfect example of the power and practicality of closures. In just ~100 lines of code (about 40 without types), it achieves the following:
- Encapsulation: Protects state from direct external access.
- Publish/Subscribe pattern: Propagates state changes efficiently via
SetanduseSyncExternalStore, and re-renders exactly the components that need it. - Simplicity: Provides an intuitive API without complex reducers, action types, or dispatchers.
Through this analysis of Zustand, I learned that a concept like closure—sometimes hard to grasp—can be used to build powerful abstractions. Understanding these patterns can help you create better state management solutions or gain deeper insight into existing libraries.