Skip to content
D
Documentation

Test Zustand stores

how-to
2 min readUpdated

Use a DOM test environment for React components and use React Testing Library to exercise the rendered component. Jest and Vitest differ in their module-loading and test-runner configuration.

When to use this pattern

Use this setup when tests create stores with create or createStore. Component tests interact with the rendered UI rather than calling implementation details.

Share the store creator

For the typed creator pattern, see Typed store patterns; this page applies that shared creator to test setup and assertions.

Configure Jest

Install the test dependencies, including ts-jest and ts-node for TypeScript configuration:

bash
npm install -D jest ts-jest ts-node jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

Add the DOM matchers and configure Jest to use JSDOM:

ts
import '@testing-library/jest-dom'
ts
const config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['./setup-jest.ts'],
}

export default config

The result is a Jest environment with a JSDOM document and Testing Library matchers.

Configure Vitest

Install the Vitest and DOM-testing dependencies:

bash
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

Enable JSDOM and the Testing Library matchers in the setup file:

text
import '@testing-library/jest-dom/vitest'

ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./setup-vitest.ts'],
  },
})

With globals disabled, import the Vitest globals used by your test files from vitest, and omit the vitest/globals type reference from global.d.ts. With globals enabled, add the Vitest type reference:

ts
/// <reference types="vite/client" />
/// <reference types="vitest/globals" />

Test a React component

The component uses the hook returned by create and selects state and the action separately. useStore is needed when a component consumes a standalone store; this component does not need it.

tsx
import { create } from 'zustand'
import { counterStoreCreator } from './shared/counter-store-creator'

export const useCounterStore = create<{
  count: number
  inc: () => void
}>()(counterStoreCreator)

export function Counter() {
  const count = useCounterStore((state) => state.count)
  const inc = useCounterStore((state) => state.inc)

  return (
    <div>
      <output aria-label="count">{count}</output>
      <button type="button" onClick={inc}>Increment</button>
    </div>
  )
}

Render the component, assert the initial UI, click the user-facing button, and assert the updated UI:

tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'

test('increments the displayed count', async () => {
  const user = userEvent.setup()
  render(<Counter />)

  expect(screen.getByLabelText('count')).toHaveTextContent('1')
  await user.click(screen.getByRole('button', { name: 'Increment' }))

  expect(screen.getByLabelText('count')).toHaveTextContent('2')
})

The test sees 1 on the initial render and 2 after the click.

Test a standalone store

For the StoreApi methods, see How Zustand works; this test uses getState() to invoke the action and inspect the resulting state.

ts
import { createStore } from 'zustand/vanilla'
import { counterStoreCreator } from './shared/counter-store-creator'

export const counterStore = createStore<{
  count: number
  inc: () => void
}>()(counterStoreCreator)
ts
import { counterStore } from './counter-store'

test('updates a standalone store through its action', () => {
  expect(counterStore.getState().count).toBe(1)

  counterStore.getState().inc()

  expect(counterStore.getState().count).toBe(2)
})

The test reads the state before and after the action.

Use a vanilla store in a component

For a component that receives or imports a standalone store, bind it with useStore and select only the value it renders.

tsx
import { useStore } from 'zustand'
import { counterStore } from './counter-store'

export function VanillaCounter() {
  const count = useStore(counterStore, (state) => state.count)
  const inc = useStore(counterStore, (state) => state.inc)

  return (
    <div>
      <output aria-label="count">{count}</output>
      <button type="button" onClick={inc}>Increment</button>
    </div>
  )
}

The hook returns the selected value and subscribes the component to changes. Test it with the same rendered interaction: the component displays 1, and the button changes it to 2.

Pitfalls

Was this page helpful?

Test Zustand stores — zustand · GPT-5.6 Luna