How do you test a custom hook in isolation, and when should you test it through a component instead?
Knowing renderHook + act for isolated hook logic, and when a real component test gives more confidence.
A custom hook can't be called outside a component (Rules of Hooks), so to test one in isolation you use renderHook from @testing-library/react, which mounts a tiny host component that calls your hook and exposes its return value via result.current. You then trigger state changes and assert on result.current. Any call that updates state must be wrapped in act() (renderHook's utilities handle this) so React flushes updates and warnings don't fire; for async updates you await the state through waitFor or the async variants. renderHook also lets you re-render with new props (rerender) to test how the hook responds to prop changes, and provides a wrapper option to supply context providers the hook depends on (a store, a router, a theme). This isolated approach is great for hooks with real LOGIC — a useDebouncedValue, a usePagination reducer, a useToggle, a data-fetching hook where you want to assert loading/error/data transitions directly. HOWEVER, prefer testing through a real component when the hook's value only makes sense in the context of UI, or when isolating it would mean asserting on implementation details. Kent C. Dodds' guidance: if a hook is part of a component's behavior, testing the component that uses it often gives more confidence and is less coupled — you verify the user-facing result rather than the intermediate return value. A pragmatic split: test complex, reusable, logic-heavy hooks in isolation with renderHook (fast, focused, covers many states); test simple hooks and hook-plus-UI interactions through the component that consumes them. Either way, avoid the trap of testing the hook's internals (which state variable holds what) rather than its observable contract (given these calls, it returns these values / drives this UI).
Deciding how to test a reusable hook; setting up renderHook with providers.
import { renderHook, act, waitFor } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => result.current.increment()); // state update -> wrap in act
expect(result.current.count).toBe(1);
});
// Provide context the hook depends on via a wrapper:
// renderHook(() => useCartTotal(), { wrapper: ({children}) =>
// <CartProvider>{children}</CartProvider> });
// Prefer a component test when the hook only matters through the UI it drives.