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:
bashnpm 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:
tsimport '@testing-library/jest-dom'
tsconst 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:
bashnpm 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:
textimport '@testing-library/jest-dom/vitest'
tsimport { 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.
tsximport { 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:
tsximport { 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.
tsimport { createStore } from 'zustand/vanilla'
import { counterStoreCreator } from './shared/counter-store-creator'
export const counterStore = createStore<{
count: number
inc: () => void
}>()(counterStoreCreator)
tsimport { 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.
tsximport { 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
- Use JSDOM for component tests. A test runner without a DOM cannot render React components.
- For selector identity and shallow-rendering pitfalls, see Selectors and rendering. For server-rendered stores, see Handle server-rendered stores.
Was this page helpful?