You have a custom React hook `useCart` that several components use. When is it right to test it directly with React Testing Library's `renderHook`, and when should you test it through a component that consumes it?
answer
- who is the consumer of this hook
- return value versus rendered output
- renderHook renders a stub component
- shared hook is a public API
- extracted hook is an implementation detail
basics
~20 sTest a hook directly when its return value is the product: a shared hook whose callers are other developers. Test it through a consuming component when the hook exists to serve one component, because there the rendered output is what people depend on.
solid answer
~50 s`renderHook` renders a minimal host component whose only job is to call the hook and expose its return value, so a direct test pins the hook's contract. That is the honest unit when the hook is shared across features and its consumers are other developers — the returned values really are its public surface. When a hook was extracted from a single component just to shorten that component, the same contract is an implementation detail: render the component and assert what a user sees, so reshaping the hook later does not break tests for a change no user could notice. I often do both where it pays: one component test proving the wiring, plus direct hook tests for combinatorial cases that would be tedious to drive through the UI. The question is whether the return value or the rendered output is the thing consumers rely on.
go deeper
Know that a hook cannot be called outside a render, and that a helper like renderHook exists precisely because of that. Be able to say what result.current holds after the hook runs.
Explain that renderHook is just a component test with a stub component, and give the criterion for choosing: shared hook with many callers versus one extracted from a single component.
Show judgment about where the test seam belongs. Point out that a green hook suite says nothing about wiring, and describe splitting coverage so the component test proves the path and the hook tests cover the input matrix.
Own the policy angle: what a team's default should be, why a blanket rule of one test file per hook inflates the suite and freezes file layout, and how you would review a hook test that only exists because the hook exists.
## What a hook test actually is A custom hook is a plain function, but it may only be called during a component's render, because it uses the framework's state and effect machinery. That single constraint explains every hook-testing helper that exists: you cannot call `useCart()` in a test the way you call `formatPrice()`. `renderHook`, which ships in `@testing-library/react` from version 13.1 onward, closes that gap by rendering a throwaway component for you. Conceptually it is no more than this: ```jsx function TestComponent({ callback, result }) { result.current = callback(); return null; } ``` It renders that component, stores whatever your callback returns in `result.current`, and hands you back `{ result, rerender, unmount }`. There is no magic and no separate hook runtime — a "hook test" is a component test whose component renders nothing and whose assertions are about a returned value instead of about the DOM. That framing is what makes the choice decidable. Both options are component tests; they differ only in whether the component under test is a real one or a stub that exists to expose internals. ## Two kinds of custom hook **The shared hook.** `useDebouncedValue`, `usePagination`, a hook published from a design system or a `packages/hooks` directory. It has many callers, often callers that do not exist yet. Its return value is the entire product: the shape of the object, when the values change, what the callbacks do. Here the return value is not an implementation detail, it is the API, and asserting on it is asserting on behaviour — the users just happen to be developers rather than end users. Testing such a hook only through one arbitrary consumer under-tests it and couples its test suite to a component that has nothing to do with it. **The extracted hook.** `useCheckoutForm`, pulled out of `CheckoutPage` last week because the component body got long. It has exactly one caller and no independent reason to exist. Its return shape is a private arrangement between two files. A direct test freezes that arrangement: rename a returned field, merge two booleans into a status string, move a piece of logic back into the component, and the tests go red although the checkout page behaves identically. That is the classic symptom of testing the wrong layer. ## What each test proves, and what it cannot A passing direct hook test proves the state machine is internally consistent: given these inputs and this sequence of calls, these values come back. It proves nothing about whether any component renders those values, passes the right arguments, or subscribes to the right provider. Teams get burned when a green hook suite sits next to a broken feature because the component was reading `cart.totalCents` while the hook returned `cart.total`. A passing component test proves the whole path a user experiences: hook plus render plus events plus wiring. It pays for that with cost per case. Driving twelve input permutations through clicks and typing is slow to write, slow to run, and hard to read; calling the hook twelve times with different arguments is trivial. ## A usable decision rule Ask who the consumer is. If the consumer is another developer who will import this hook, the return value is the observable behaviour — test it directly. If the consumer is a user looking at a screen, the rendered output is the observable behaviour — test through the component. If the honest answer is "both", write both, but split them by purpose rather than duplicating: the component test covers the happy path and the wiring; the hook tests cover the matrix of inputs and edge states. A second, blunter check: could you delete this hook by inlining it back into its component without changing anything a user sees? If yes, its dedicated test file is measuring your file layout. ## Practical notes - A hook test still needs state updates wrapped so the framework can flush them; the point of interest is the value you read back afterwards, not the wrapper. - `unmount` from `renderHook` is how you test a hook's cleanup — the listener it removes, the request it aborts — which is one of the few things genuinely awkward to observe through a component. - Never test a hook by mocking it. Mocking `useCart` in a component test and mocking the component in a hook test leaves nobody testing the seam between them. - Resist the rule "every hook gets a test file". Coverage of hooks is not the goal; coverage of behaviour is, and some hooks are covered perfectly well by the component that is their only reason for existing.
- What does `renderHook` actually do under the hood?It renders a minimal component whose body just calls your callback, and stores that call's return value in `result.current`, re-storing it on every render. You also get `rerender` to re-invoke the callback with new arguments and `unmount` to trigger cleanup. It is an ordinary component render with an ordinary component you never see.
- A team's hook tests are all green but the feature is broken in the app. What class of bug does hook-level testing structurally miss?Wiring bugs. The hook's contract can be perfect while the component passes the wrong argument, reads a field that no longer exists, renders the value into the wrong place, or sits outside the provider the hook needs. Nothing that only exercises the hook in a stub component can see any of that; you need at least one test that renders the real consumer.
- Does testing a hook's returned values directly contradict the advice to test behaviour rather than implementation?Only if you have misidentified the consumer. For a hook published for other developers, the returned object is the interface they program against, so asserting on it is asserting on behaviour. For a hook extracted from one component, the same assertions pin a private detail, which is exactly the failure the advice warns about. The rule is unchanged; the boundary moved.
saying these in an interview costs you the question
- Every custom hook deserves its own dedicated test file
- A hook can be called directly in a test like a normal function
- Direct hook tests are always better than component tests
- A green hook test means the component using it works
- Mocking the hook in the component test still covers the feature