How to Test Zustand Stores with Jest and React Testing Library: Complete Guide
You can test Zustand stores by creating a Jest mock that captures initial state and resets stores after each test, then using React Testing Library to verify component behavior while the mock ensures test isolation.
Testing Zustand stores in React applications requires understanding the library's modular architecture. The pmndrs/zustand repository exposes a vanilla core (src/vanilla.ts) and React bindings (src/react.ts) that you can target with different testing strategies depending on whether you are verifying pure state logic or component integration.
Understanding Zustand's Testing Architecture
Zustand stores are plain JavaScript objects with a minimal API surface: getState, setState, subscribe, and getInitialState. This framework-agnostic design allows you to test business logic directly or through React components.
Core Store Implementation
The vanilla store implementation in src/vanilla.ts defines the fundamental behavior. When you call createStore, you receive an object that holds state and mutators. This layer has no React dependencies, making it ideal for pure unit tests.
React Bindings
The src/react.ts file exports the useStore hook, which internally uses React.useSyncExternalStore to subscribe to store slices. When testing components that consume stores via hooks like useCounterStore, you are exercising this binding layer.
The Mock Pattern for Test Isolation
The recommended approach in the official documentation (docs/learn/guides/testing.md) uses a Jest/Vitest mock that wraps the real create function. This mock records each store's initial state in a storeResetFns Set and restores it after every test, preventing state bleed-through between test cases.
Setting Up the Jest Mock for Zustand
Create a mock file at __mocks__/zustand.ts that intercepts store creation and registers reset functions. The mock preserves the original behavior while adding test lifecycle management.
// __mocks__/zustand.ts
import { act } from '@testing-library/react'
import type * as ZustandExportedTypes from 'zustand'
export * from 'zustand'
const { create: actualCreate, createStore: actualCreateStore } =
jest.requireActual<typeof ZustandExportedTypes>('zustand')
export const storeResetFns = new Set<() => void>()
const createUncurried = <T>(stateCreator: ZustandExportedTypes.StateCreator<T>) => {
const store = actualCreate(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => store.setState(initialState, true))
return store
}
export const create = ((stateCreator) => {
return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried
}) as typeof ZustandExportedTypes.create
After each test, iterate through the reset functions to restore initial state. Jest's module system automatically replaces the real zustand export with this shim when you configure jest.config.ts to use the mock.
afterEach(() => {
act(() => {
storeResetFns.forEach((reset) => reset())
})
})
Creating Reusable Store Logic
Extract your store definition into a shared creator function to ensure both your application and tests import identical logic. This pattern separates the state definition from the hook instantiation.
// shared/counter-store-creator.ts
import { type StateCreator } from 'zustand'
export type CounterStore = {
count: number
inc: () => void
}
export const counterStoreCreator: StateCreator<CounterStore> = (set) => ({
count: 1,
inc: () => set((s) => ({ count: s.count + 1 })),
})
Then create the bound hook for your components:
// stores/use-counter-store.ts
import { create } from 'zustand'
import { counterStoreCreator, type CounterStore } from '../shared/counter-store-creator'
export const useCounterStore = create<CounterStore>()(counterStoreCreator)
Writing Tests with React Testing Library
Configure Jest with a jsdom environment and import testing library helpers in your setup file. The mock automatically resets store state between assertions, so each test starts fresh.
// jest.config.ts
import type { JestConfigWithTsJest } from 'ts-jest'
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['./setup-jest.ts'],
}
export default config
Write component tests that verify both the UI and store integration. The mock ensures the initial count returns to 1 for every test case.
// Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from '../../components/counter/counter'
test('increments the counter when button is clicked', async () => {
render(<Counter />)
expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('1')
await userEvent.click(screen.getByRole('button', { name: /one up/i }))
expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('2')
})
Unit Testing Vanilla Stores
For logic that does not require React, test the vanilla store directly using createStore from zustand/vanilla. This approach bypasses the hook layer and tests pure state management.
// counter-logic.test.ts
import { createStore } from 'zustand/vanilla'
import { counterStoreCreator } from './shared/counter-store-creator'
it('increments count through the store API', () => {
const store = createStore(counterStoreCreator)
expect(store.getState().count).toBe(1)
store.getState().inc()
expect(store.getState().count).toBe(2)
})
This pattern is particularly useful for testing complex selectors or middleware that operate on the core store object defined in src/vanilla.ts.
Summary
- Use a Jest mock at
__mocks__/zustand.tsto capture initial state and reset stores after each test viastoreResetFns. - Separate store logic into reusable
StateCreatorfunctions that can be tested independently or through React components. - Test React integration with React Testing Library, relying on the mock to ensure test isolation without manual cleanup.
- Test vanilla logic directly by importing
createStorefromzustand/vanillawhen you need to verify state mutations without the UI layer. - Reference the source in
src/react.tsandsrc/vanilla.tsto understand howuseSyncExternalStorebinds components to the underlying store.
Frequently Asked Questions
How do I prevent Zustand store state from leaking between tests?
Create a mock that records each store's getInitialState() when the store is first created, then call setState(initialState, true) in an afterEach hook. Wrap the reset calls in act() from React Testing Library to ensure proper React lifecycle handling. This pattern is documented in docs/learn/guides/testing.md and handles cleanup automatically.
Can I test Zustand stores without React Testing Library?
Yes. Import createStore from zustand/vanilla to instantiate stores directly. Since the vanilla API exposes getState, setState, and subscribe methods, you can write pure unit tests using Jest assertions without rendering components. This is the approach used in tests/vanilla/basic.test.ts within the repository.
Why does the Jest mock use jest.requireActual?
The mock uses jest.requireActual<typeof ZustandExportedTypes>('zustand') to preserve the original implementation while adding testing instrumentation. This ensures your tests run against the real store logic while the shim captures references to reset functions. The mock maintains type safety by importing the actual module types and casting the wrapper function appropriately.
How do I test components that use multiple Zustand stores?
The mock pattern works transparently with multiple stores. Each call to create registers its own reset function in the shared storeResetFns Set. When afterEach runs, it iterates through all registered stores and resets each to its initial state, ensuring complete isolation even when components consume several different stores simultaneously.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →