hook-test — test a hook the ReactUse way
The core package uses jest (not vitest), testEnvironment: 'jsdom'. Tests run via
babel-jest against src/ directly — no build step needed.
Where tests live
Co-located with the hook: packages/core/src/useX/index.spec.ts (a couple use .test.ts,
but prefer index.spec.ts). There is no central __tests__ directory.
Basic pattern
import { act, renderHook } from '@testing-library/react'
import { useX } from '.'
describe('useX', () => {
function setUp(initial?: number) {
return renderHook(() => useX(initial))
}
it('inits with the given value', () => {
const { result } = setUp(5)
expect(result.current[0]).toBe(5)
})
it('updates on set', () => {
const { result } = setUp(0)
act(() => {
result.current[1](10) // wrap every state mutation in act()
})
expect(result.current[0]).toBe(10)
})
})
Key rules:
- Wrap a
renderHookcall in a smallsetUp()helper. - Every state mutation goes inside
act(() => { … }). - Read current values via
result.current; usererender()/unmount()from therenderHookreturn for prop changes and cleanup assertions.
Timer hooks
describe('useInterval', () => {
jest.useFakeTimers()
jest.spyOn(global, 'clearInterval')
it('fires on the interval', () => {
const cb = jest.fn()
renderHook(() => useInterval(cb, 20))
expect(cb).not.toBeCalled()
jest.advanceTimersByTime(70)
expect(cb).toHaveBeenCalledTimes(3)
})
it('clears on unmount', () => {
const { unmount } = renderHook(() => useInterval(jest.fn(), 200))
unmount()
expect(clearInterval).toHaveBeenCalledTimes(1)
})
})
SSR / hydration tests
For hooks that must not break SSR or cause hydration mismatch, use the custom environment and render on the server, then hydrate:
/**
* @jest-environment ./.test/ssr-environment
*/
import { act } from '@testing-library/react'
import ReactDOMServer from 'react-dom/server'
import ReactDOMClient from 'react-dom/client'
import { createMockMediaMatcher } from '../../.test'
import { createTestComponent } from '../../.test/testingHelpers'
describe('useX SSR', () => {
beforeEach(() => {
jest.resetModules()
window.matchMedia = createMockMediaMatcher({ '(prefers-color-scheme: dark)': true }) as any
})
it('does not mismatch during hydration', async () => {
const TestComponent = createTestComponent(() => useX())
const el = document.createElement('div')
const markup = ReactDOMServer.renderToString(<TestComponent />)
el.innerHTML = markup
const root = await act(() => ReactDOMClient.hydrateRoot(el, <TestComponent />))
expect(el.innerHTML).toBe(markup)
await act(() => root.unmount())
})
})
Reference: packages/core/src/usePreferredDark/index.ssr.spec.tsx.
Helpers (packages/core/.test/)
createMockMediaMatcher(matches)andcreateMockRaf()—../../.testcreateTestComponent(hook),sleep(ms),request(req)—../../.test/testingHelpersjest-setup.tsadds atoHaveErrorMatchingmatcher and awindow.testErrorsarray.
Running
pnpm --filter @reactuses/core test useX # single hook (jest name pattern)
pnpm --filter @reactuses/core test # whole package
pnpm --filter @reactuses/core test:coverage # with coverage
When fixing a failing test: run it first to see the real error, read the hook source and at least one call site, make the minimal fix, then re-run green. No speculative edits.