# How to Use a Zustand Vanilla Store Without React for Non-UI State

> Learn to use Zustand vanilla store without React for non-UI state management in Node.js or workers. Effortlessly handle global state outside your components.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Import `createStore` from `zustand/vanilla` to instantiate a framework-agnostic store that exposes `getState`, `setState`, `subscribe`, and `getInitialState` for use in Node.js, workers, or any non-UI context.**

Zustand is not inherently tied to React. The `pmndrs/zustand` repository provides a vanilla API that lives in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts), allowing you to use Zustand vanilla store without React for server-side logic, background workers, or Electron main processes. This guide explains the core implementation details and practical patterns for managing non-UI state.

## What Is the Zustand Vanilla Store?

The vanilla store is the underlying engine that powers Zustand’s React integration. When you use `createStore` (lines `L99-L107` in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)), you receive a plain JavaScript object with a minimal CRUD API:

| Method | Description |
|--------|-------------|
| **`setState`** | Mutates the store. Shallow-merges objects by default; pass `true` as the second argument to replace the entire state. |
| **`getState`** | Returns the current state snapshot (or the result of a selector). |
| **`getInitialState`** | Returns the state produced by the initializer before any mutations. |
| **`subscribe`** | Registers a listener `(newState, previousState) => void`. Returns an unsubscribe function. |

Because there is no React hook involved, the store can be imported and used anywhere JavaScript runs.

## Core API Methods in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)

### createStore Implementation

The `createStore` function (lines `L60-L69`) is a thin wrapper around `createStoreImpl`. It invokes your initializer with three utilities—`setState`, `getState`, and the store API—and stores the resulting state in a private variable (lines `L63-L66`). Listeners are tracked in a `Set` (lines `L64-L65`) for efficient subscription management.

### setState and State Updates

The `setState` logic (lines `L70-L80`) handles both functional and object updates:

- **Functional updates**: If you pass a function, it receives the current state and returns the next state.
- **Merge vs. Replace**: By default, `setState` shallow-merges objects. If you pass `true` as the second argument (lines `L75-L78`), or if the new value is not an object, the state is replaced outright.
- **Equality check**: Listeners are only notified if the new state differs from the previous one using `Object.is` (line `L73`).

### Subscription Handling

The `subscribe` method (lines `L88-L92`) adds a listener to the internal `Set`. On every successful state update, the store iterates through listeners (line `L79`) and invokes them with `(newState, previousState)`. The `subscribe` call returns a function that removes the listener when called (line `L91`).

### getInitialState

The `getInitialState` method (lines `L85-L87`) caches the state produced by the initializer at creation time. This is useful for resetting or comparing against the original state after mutations have occurred.

## Practical Examples: Using Zustand Without React

### Basic Store Creation and Usage

Create a store in a standalone file and import it anywhere:

```typescript
// store.ts
import { createStore } from 'zustand/vanilla'

export const counterStore = createStore<{ count: number; inc: () => void }>((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

```

```typescript
// worker.ts (Node.js or Web Worker)
import { counterStore } from './store'

// Read state
console.log(counterStore.getState().count) // 0

// Mutate state
counterStore.getState().inc()
console.log(counterStore.getState().count) // 1

// Subscribe to changes
const unsubscribe = counterStore.subscribe((newState, prevState) => {
  console.log(`Changed from ${prevState.count} to ${newState.count}`)
})

counterStore.getState().inc() // Logs: Changed from 1 to 2
unsubscribe() // Stop listening

```

### Replacing State vs. Merging

By default, `setState` shallow-merges objects. To replace the entire state object, pass `true` as the second argument:

```typescript
import { createStore } from 'zustand/vanilla'

type State = { a?: number; b?: number }
const store = createStore<State>(() => ({ a: 1 }))

// Merge (default)
store.setState({ b: 2 })
console.log(store.getState()) // { a: 1, b: 2 }

// Replace
store.setState({ b: 2 }, true)
console.log(store.getState()) // { b: 2 } (a is gone)

```

This corresponds to the `replace` logic at lines `L75-L78` in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts).

### Working with Primitive Values

The vanilla store supports primitive state values, not just objects:

```typescript
import { createStore } from 'zustand/vanilla'

const numberStore = createStore<number>(() => 0)

// Functional update
numberStore.setState((n) => n + 5)
console.log(numberStore.getState()) // 5

// Replace with primitive
numberStore.setState(10, true)
console.log(numberStore.getState()) // 10

```

The `setState` implementation (lines `L70-L72`) handles both functional and direct value updates.

### Accessing Initial State

Retrieve the original state for reset functionality or comparison:

```typescript
import { createStore } from 'zustand/vanilla'

const initialState = { count: 0, name: 'default' }
const store = createStore<typeof initialState>(() => initialState)

// Mutate
store.setState({ count: 5, name: 'updated' })

// Access initial
console.log(store.getInitialState()) // { count: 0, name: 'default' }
console.log(store.getState())        // { count: 5, name: 'updated' }

```

This uses the `getInitialState` method defined at lines `L85-L87` in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts).

### Optional React Integration

If you later need to use the same store in a React component, bind it with the `useStore` hook without recreating the logic:

```tsx
// reactBinding.tsx
import { useStore } from 'zustand'
import { counterStore } from './store'

export const useCounter = <T,>(selector: (state: typeof counterStore.getState()) => T) => 
  useStore(counterStore, selector)

// Component usage
function Counter() {
  const count = useCounter((s) => s.count)
  const inc = useCounter((s) => s.inc)
  return <button onClick={inc}>Count: {count}</button>
}

```

The `useStore` hook in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) wraps the vanilla store's `subscribe` and `getState` methods for React integration.

## File Structure and Source References

Understanding the source layout helps when debugging or extending the vanilla store:

| File | Purpose | Link |
|------|---------|------|
| [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) | Core `createStore` implementation, state API, and subscription handling. | [view](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) |
| [`src/vanilla/shallow.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla/shallow.ts) | Shallow-equality helper used by selectors and middleware. | [view](https://github.com/pmndrs/zustand/blob/main/src/vanilla/shallow.ts) |
| [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) | `useStore` hook that binds vanilla stores to React components. | [view](https://github.com/pmndrs/zustand/blob/main/src/react.ts) |
| `src/middleware/*` | Optional middleware (e.g., `persist`, `immer`, `devtools`) compatible with vanilla stores. | [view](https://github.com/pmndrs/zustand/tree/main/src/middleware) |
| [`tests/vanilla/basic.test.ts`](https://github.com/pmndrs/zustand/blob/main/tests/vanilla/basic.test.ts) | Test suite demonstrating the vanilla API in practice. | [view](https://github.com/pmndrs/zustand/blob/main/tests/vanilla/basic.test.ts) |

## Summary

- **Import from `zustand/vanilla`**: Use `createStore` instead of the default `create` to eliminate React dependencies and reduce bundle size.
- **Four core methods**: Control state with `getState`, `setState`, `subscribe`, and `getInitialState` as implemented in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts).
- **Flexible updates**: `setState` shallow-merges objects by default; pass `true` as the second argument to replace the entire state (lines `L75-L78`).
- **Framework-agnostic**: Use the same store in Node.js scripts, Web Workers, or Electron main processes without React.
- **React compatible**: Bind an existing vanilla store to React later using the `useStore` hook from `zustand` without rewriting logic.

## Frequently Asked Questions

### Can I use a Zustand vanilla store in Node.js?

Yes. Because `zustand/vanilla` has no React dependencies, you can import `createStore` into any Node.js script or server-side application. The store works identically in Node.js, Deno, Bun, or browser workers as it does in the browser main thread.

### What is the difference between `create` and `createStore`?

`create` (the default export from `zustand`) is a convenience wrapper that creates a vanilla store and immediately binds it to React via the `useStore` hook. `createStore` (from `zustand/vanilla`) returns the raw store object with `getState`, `setState`, and `subscribe`, giving you explicit control without React coupling. Use `createStore` when you need to use Zustand vanilla store without React.

### How do I subscribe to state changes without React?

Call the `subscribe` method on your store instance and pass a callback that receives `(newState, previousState)`. The method returns an unsubscribe function:

```typescript
const unsubscribe = store.subscribe((newState, prevState) => {
  console.log('State changed:', newState)
})
// Later: unsubscribe()

```

This subscription mechanism (implemented in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) at lines `L88-L92`) is synchronous and fires on every state mutation.

### Can I use middleware with the vanilla store?

Yes. Middleware like `persist`, `immer`, or `devtools` can wrap a vanilla store created with `createStore`. Since the vanilla API is the foundation of all Zustand stores, middleware functions that accept a store creator work identically whether you use React or not. Import middleware from `zustand/middleware` and apply it to your vanilla store creator.