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
state
variable is “trapped” inside the scope ofcreateStoreImpl
. From the outside, it can only be accessed through API methods likegetState
orsetState
.- State Persistence: Although
createStoreImpl
is invoked only once, the resultingstate
andlisteners
persist 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
useSyncExternalStore
viauseBoundStore
. - 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
create
runscreateStoreImpl
. - At this time, a unique
state
andlisteners
foruseBearStore
are created in the closure scope. state
is initialized as{ bears: 0, increase: [Function] }
. Theincrease
function rememberssetState
through the closure.- A custom hook named
useBearStore
is returned. This hook also carries methods likesetState
andgetState
.
Step 2: Component render and subscription (useBearStore
)
- When the
BearCounter
component first renders, theuseBearStore
hook runs. useSyncExternalStore
is called, and a re-rendering function is registered in thelisteners
created in Step 1 viasubscribe
.- The
getSnapshot
callback() => state.bears
runs, 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
increase
function obtained in theControls
component runs. increase
callssetState
, which it remembers via closure.setState
updatesstate
to{ bears: 1 }
.setState
iterates overlisteners
and calls every registered function.- The 're-render function' registered by
useSyncExternalStore
inBearCounter
is called. - React re-renders the
BearCounter
component, runsgetSnapshot
again, 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
Set
anduseSyncExternalStore
, 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.