patterntypescriptvitestTip
Test React client component effects in Vitest without jsdom by mocking React hooks
Viewed 0 times
hooks mockno jsdomuseEffect testdialog showModalpopover
Problem
A Next.js/React 19 project runs Vitest in the node environment with no jsdom/happy-dom and adding dependencies is not allowed. renderToStaticMarkup never runs effects or event handlers, so behaviour (dialog closing when a prop changes, toast timers, onBlur/onToggle handlers) survives mutations untested.
Solution
In a dedicated test file, vi.mock("react") spreading the real module but replacing useRef/useState/useEffect/useLayoutEffect/useId/useSyncExternalStore with a tiny hook engine (one slot per hook call index, effects queued and run on demand, setState re-renders synchronously). Call the component as a plain function to get its element tree, walk props.children to find elements, assign fake DOM objects to props.ref.current (fake <dialog> whose close() fires onClose, fake popover with matches(':popover-open')/hidePopover), then call handlers (onClick, onBlur, onToggle) and run effects. Use vi.useFakeTimers for timers and vi.stubGlobal for matchMedia/localStorage. Keep static-markup tests in another file, because the react mock would also break react-dom/server.
Why
JSX compiles to react/jsx-runtime, which is a separate specifier loaded natively from node_modules and is not affected by vi.mock("react"), so elements are still created normally while source files importing hooks from "react" get the fake engine.
Gotchas
- Spreading a fake object (…fake) copies methods whose closures still point at the original object; use Object.assign(fake, extra) to keep identity.
- In React 19 the ref is a regular prop: element.props.ref, and keys are on element.key, not props.
- Do not import react-dom/server in the same file: it would get the mocked react.
Revisions (0)
No revisions yet.