skip to content

A custom hook you want to test reads from a context provider that your test does not render, so it throws the moment `renderHook` runs it. What are your options, and what does the amount of setup tell you?

level: seniorimportance: should knowfreq 40%

answer

  1. the stub renders with nothing above it
  2. wrapper supplies the real provider
  3. prefer real provider over a stubbed module
  4. setup larger than the assertion is a signal
  5. arguments are testable, ambient state is not

basics

~20 s

Pass a wrapper component to renderHook that renders the real provider with test-controlled values, or test through a component that already sits inside that provider tree. If the wrapper has to rebuild half the app, the hook is coupled to ambient context and the test is an integration test in disguise.

solid answer

~50 s

`renderHook` accepts a `wrapper` option — a component receiving `children` — and the stub component that calls the hook is rendered inside it, so wrapping in the real provider with values you control is the direct fix. I prefer the real provider over stubbing the context module, because a stub makes the test pass while proving nothing about whether the hook and the provider still agree. If the wrapper starts stacking a router, a query client, an auth provider and a theme just to get one hook to run, that is the signal worth reacting to: the hook depends on ambient state, the setup is now larger than the assertion, and I would either test through a real component that legitimately lives in that tree, or change the hook to take the dependency as an argument so it becomes trivially testable. A shared render helper is fine for genuinely global providers — it should not become a place to hide coupling.

code

javascript · 15 lines
javascript
import { renderHook } from '@testing-library/react';
import { SettingsProvider } from './SettingsProvider';
import { useCurrencyFormatter } from './useCurrencyFormatter';

test('formats using the currency from settings', () => {
  const wrapper = ({ children }) => (
    <SettingsProvider value={{ currency: 'EUR', locale: 'de-DE' }}>
      {children}
    </SettingsProvider>
  );

  const { result } = renderHook(() => useCurrencyFormatter(), { wrapper });

  expect(result.current(1234.5)).toBe('1.234,50 €');
});

go deeper

for a junior

Know that a hook reading context needs that provider present in the test, and that the helper accepts a wrapper component which renders children inside the provider.

for a middle

Explain why the stub component mounts with nothing above it, and contrast supplying the real provider with stubbing the context module, saying what each choice can still catch.

for a senior

Demonstrate judgment about the seam: read the size of the setup as design feedback, choose deliberately between an honest integration test through a component and reducing the hook's ambient dependencies, and name the order-dependence risk shared providers introduce.

for a principal

Own the codebase-level policy: what belongs in a shared render helper, how you keep it from quietly absorbing coupling, and how you would steer hook design so testability is a byproduct rather than a negotiation.

## Why it throws A hook that reads context is not self-contained: it is a function of state that lives above it in the tree. In the app that state is always there, because the provider is mounted at the root. In a test the stub component that `renderHook` creates is mounted alone, with nothing above it, so a hook that reads a required context gets whatever default the context was created with — often `undefined`, and well-written hooks throw a deliberate error saying they must be used inside their provider. That error is a feature. It is telling you something true about the unit you are trying to isolate: it cannot be isolated without supplying its environment. ## Option 1 — wrap it in the real provider The `wrapper` option takes a component that receives `children` and renders them; the harness mounts your stub inside it. ```jsx const wrapper = ({ children }) => ( <SettingsProvider value={{ currency: 'EUR' }}>{children}</SettingsProvider> ); const { result } = renderHook(() => useCurrencyFormatter(), { wrapper }); ``` This keeps the real provider in the loop, so the test breaks if the provider's value shape and the hook's expectations drift apart — which is one of the few genuinely useful things this test can catch. Prefer providers that let you inject a value directly; when the only way in is a provider that fetches its own data, the provider itself is the thing that needs redesigning. ## Option 2 — test through a component If a real consumer of the hook already renders inside that provider tree, testing the consumer costs nothing extra and covers strictly more: the hook, the provider wiring, and the component that reads the result. This is usually the better trade for an app hook that has one caller and is inseparable from its context anyway. ## Option 3 — change the hook The most valuable outcome of a painful test is often a change to the code under test. A hook that takes what it needs as an argument — `useCurrencyFormatter(currency)` — is testable with no wrapper at all, and the context read moves to the one component that actually sits under the provider. Not every hook should be reshaped this way; a hook whose whole purpose is to expose context is doing exactly its job. But a hook that reaches into three ambient providers to do a computation is hiding its inputs, and the test is the first place that shows up. ## What not to do **Do not stub the context module.** Replacing the module so the hook receives a fake removes the only participant that could have disagreed with it. The test then asserts that the hook works against a value the test itself invented, and it will stay green through a provider refactor that breaks production. **Do not build a wrapper that recreates the app.** When the wrapper stacks a router, a data-fetching client, an auth provider, a feature-flag provider and a theme, several things have gone wrong at once: the setup is now longer and more likely to be wrong than the assertion; every one of those providers is a shared mutable dependency that must be reset between tests or the suite develops order dependence; and the label "unit test" has stopped being accurate without anyone deciding to make it an integration test. At that point choose deliberately — either accept it as an integration test of the real component, or reduce the hook's dependencies. **Do not let the shared helper hide it.** Most codebases grow a single `renderWithProviders` used everywhere. That is reasonable for a small number of genuinely global providers, and it stops each test from re-deriving the tree. The failure mode is that it also stops anyone from noticing that a hook now needs five providers, because the cost is paid once in a file nobody reads. Reviewing what goes into that helper is how a team keeps the signal. ## The transferable principle Ambient dependencies are the thing that decides where a test seam can go. A unit that reads its inputs from arguments can be tested anywhere; a unit that reads them from its surroundings can only be tested by reconstructing those surroundings, and the reconstruction is itself code that can be wrong. When a test's setup dwarfs its assertion, treat that as design feedback about the unit rather than as a fact of life about testing.

  • Why prefer wrapping in the real provider over replacing the context module with a fake?
    Because the fake removes the only party that could contradict the hook. With the real provider in the tree, a change to the value shape breaks the test, which is one of the few defects this test can genuinely catch. With a module stub, the hook is checked against a value the test invented, and a provider refactor that breaks production leaves the suite green.
  • Your team's shared renderWithProviders helper has grown to six providers. What is the actual harm?
    Two things. Every test now carries shared, mutable dependencies that must be reset between cases or the suite becomes order-dependent and intermittently red. And the helper hides growth in coupling: nobody notices that a unit needs six ambient dependencies, because the cost is paid once in a file no one reviews. Keep it to genuinely global providers and review additions.
  • When is a context-reading hook not a design problem at all?
    When exposing that context is the hook's entire purpose — a thin `useAuth` or `useTheme` whose job is to read the provider and throw a helpful error outside it. Wrapping such a hook in its own provider for a test is honest and cheap. The smell is a hook that reaches into several unrelated providers to perform a computation, hiding its real inputs.

saying these in an interview costs you the question

  • Mock the context module so the hook stops throwing
  • Any amount of provider setup is normal for hook tests
  • The provider error means the test helper is broken
  • A shared render helper makes coupling a non-issue
  • Hooks should always read config from context, never arguments

context