Testing React Hooks

Custom hooks can be tested by observing their returned state, invoking exposed actions, and checking how they interact with dependencies. The goal is to verify externally observable behavior rather than implementation details such as internal hook calls.

For custom hooks, `renderHook` from React Testing Library provides a small test environment in which the hook can execute according to React's rules. The returned value is exposed through `result.current`, allowing a test to inspect state and call functions returned by the hook.

When an interaction causes React state to update, perform it inside `act`. This tells the test environment that React updates are expected and ensures assertions run after the relevant updates have been processed.

Hooks that depend on context can be tested with a `wrapper` component that supplies the required provider. Hooks that depend on external services, timers, or browser APIs should generally receive controllable dependencies or use appropriate test doubles so the test focuses on the hook's behavior.

A useful test usually covers the initial state, an important state transition, and behavior around dependency changes or cleanup. Avoid asserting private implementation details such as the exact number of `useState` calls when the same behavior can be verified through the hook's public return value.

Key points

Interview questions

How would you test a custom React hook?

I would render the hook with `renderHook` and inspect its returned value through `result.current`. I would verify the initial state, invoke an exposed operation inside `act`, and assert the resulting state. If the hook needs context, I would provide the relevant provider through a wrapper. I would focus on behavior that consumers of the hook can observe.

Why do hook tests use `act` around state-changing operations?

`act` marks a section of a test as one in which React updates are expected to occur. It gives React and the test environment an opportunity to process the resulting updates before assertions are made. Without it, a test can observe stale state or produce warnings about updates occurring outside the expected testing workflow. The important rule is to wrap interactions that cause React state or effect-driven updates.

How would you test a custom hook that requires a context provider?

I would create a wrapper that renders the required provider around its children and pass that wrapper to `renderHook`. The test can then exercise the hook as a consumer would, including changing provider inputs when that behavior matters. If the provider itself is complex, I would keep the test focused on the contract the hook relies upon rather than duplicating unrelated provider implementation. This makes the test representative without making it unnecessarily broad.

How would you test a hook that performs an asynchronous operation?

I would render the hook, trigger the operation through its public API, and wait for the observable state transition using the async utilities provided by the testing library. External requests or services would normally be controlled with a deterministic test boundary rather than relying on an actual remote system. I would verify meaningful states such as loading, success, and failure where those states are part of the hook's contract. I would also check cleanup or cancellation behavior if the hook starts work that can outlive a render.

What makes a hook test too implementation-focused?

A test becomes implementation-focused when it asserts internal hook structure rather than behavior that matters to callers. For example, asserting the number of internal state hooks or effect registrations creates coupling to refactoring that should not change the public contract. A stronger test invokes the hook's exposed operations and verifies returned values, externally visible effects, and interactions with meaningful dependencies. This allows the implementation to change while preserving confidence in behavior.