This episode covers zustand/vanilla: creating stores without hooks with createStore, using getState and subscribe in non-React code like Node services and web workers, then connecting the vanilla store back to React with useStore. One store can be used from both worlds with a single source of truth.

One of Zustand's architecture advantages is a core that doesn't depend on React. Episode 17 covers zustand/vanilla: creating stores without hooks, using getState and subscribe in non-React code like Node services or web workers, then connecting the vanilla store to React components with useStore.
You'll see why a single store can be used from two different worlds with one source of truth.
The zustand/vanilla module exposes createStore, which returns a store API instead of a hook:
import { createStore } from 'zustand/vanilla'
interface CounterState {
count: number
increment: () => void
}
export const counterStore = createStore<CounterState>()((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
counterStore.getState()
counterStore.subscribe((s) => console.log('count:', s.count))createStore from zustand/vanilla returns an object with getState, setState, subscribe, and getInitialState. There's no JSX, no hooks, and no React inside it — purely a state container you can import anywhere.
Because it doesn't use React, a vanilla store can be used in a Node service that processes queues or events. Here's a listener responding to state changes from outside:
import { counterStore } from './counter-store.js'
const unsubscribe = counterStore.subscribe((state) => {
if (state.count > 10) {
notifyAdmin('counter melewati batas')
}
})
process.on('SIGINT', unsubscribe)counterStore.subscribe(listener) calls the listener with the full state on every change. This is a simple event bus: business logic reads state and makes decisions without needing a UI.
In a web worker, the state held by the worker can't automatically share with the main thread. A vanilla store inside the worker becomes the local source of truth:
import { createStore } from 'zustand/vanilla'
const workerStore = createStore((set) => ({
total: 0,
add: (n) => set((s) => ({ total: s.total + n })),
}))
self.onmessage = (event) => {
workerStore.getState().add(event.data)
self.postMessage({ total: workerStore.getState().total })
}workerStore.getState().add(event.data) mutates state in the worker, then the result is sent back to the main thread. A vanilla store removes the need to manage a mutable global manually in the worker.
A vanilla store can be used directly in React with useStore from zustand. Components subscribe to the same store so selective re-rendering still works:
import { useStore } from 'zustand'
import { counterStore } from './counter-store'
export function Counter() {
const count = useStore(counterStore, (s) => s.count)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => counterStore.getState().increment()}>
Tambah
</button>
</div>
)
}useStore(counterStore, (s) => s.count) takes the vanilla store and a selector like a regular hook. The result: state logic in a separate file that can be tested without React, and a React UI that subscribes with full performance — one store, two consumers.
Episode 17 opens Zustand's door out of React: createStore produces a vanilla store used in Node services and web workers via getState and subscribe, then connected back to React with useStore without rewriting the store.
Key takeaways:
In the next episode we will discuss testing stores and middleware — writing unit tests with Vitest for set and get state, testing async actions and the persist middleware with mocked storage, and component testing with React Testing Library.