Testing Hooks & State
Chapter 4 tested components as black boxes through their rendered output. This chapter covers two more specific situations: testing a custom hook in isolation — which has no JSX of its own at all — and testing components whose correctness depends on useState/useEffect timing, including controlled forms and asynchronous updates.
Testing Custom Hooks with renderHook
A custom hook like useCounter isn't a component — render() and screen don't apply. renderHook mounts a hook inside a minimal, invisible test component behind the scenes, returning a result object whose .current holds the hook's live return value:
act(...) tells React "a state update just happened here — flush it and re-render before continuing." Without it, an assertion immediately after calling an updater function can read a stale value from before React has actually processed the update.
Testing Components That Use useState
A controlled input's displayed value depends on component state that changes as the user types — combining Chapter 4's userEvent.type with a query that reads the input's current value:
Testing useEffect Timing
A component that fetches data in useEffect doesn't show its result synchronously — this is exactly where Chapter 3's findBy and the more general waitFor matter. findBy* is a convenience wrapper built around one query; waitFor(callback) is general-purpose — it re-runs an arbitrary assertion callback until it passes or times out, useful when a check can't be expressed as a single query.
Controlled Forms End to End
Combining everything: typing into multiple controlled inputs, submitting, and confirming an async result:
renderHook
Mounts a custom hook with no JSX of its own; result.current holds its live return value.
act()
Flushes a state update before assertions continue — required when calling an updater outside RTL's own built-in helpers.
findBy* (simple)
A single query, built-in retry — the right choice when one thing needs to eventually appear.
waitFor (flexible)
Wraps an arbitrary assertion callback — the right choice when multiple conditions need checking together.
act(...) calls everywhere — but render, fireEvent, and userEvent now wrap themselves in act internally. Explicit act() is mainly needed for renderHook's own updater calls (as above) or when triggering a state change through something entirely outside RTL's built-in helpers.
findBy* and waitFor return Promises. Forgetting await doesn't throw a clear error — it lets the test's assertions run against the wrong moment in time, before the update has actually finished. The result is a test that sometimes passes and sometimes fails depending on timing — a genuinely nasty class of bug to diagnose, and the exact same category of mistake as Chapter 4's forgotten-await-on-userEvent gotcha, just showing up again here.
Coding Challenges
Write a renderHook test for a custom hook useToggle() that returns { value, toggle }, starting at false, confirming that calling toggle() once sets value to true and calling it again sets it back to false.
📄 View solutionWrite a test for a component that fetches a list of items on mount (in useEffect) and shows "Loading..." until they arrive. Use findByText to confirm an item eventually appears, and queryByText to confirm "Loading..." is gone by then.
📄 View solutionExplain why forgetting await on a findByText call can make a test pass sometimes and fail other times, rather than failing consistently and obviously — referencing what the test actually does if the Promise is never awaited.
📄 View solutionChapter 5 Quick Reference
- renderHook(() => useX()) — mounts a custom hook;
result.currentholds its live value - act(() => { ... }) — flushes a state update; needed for renderHook updater calls, largely automatic elsewhere now
- toHaveValue(...) — asserts a controlled input's current value
- findBy* — single query, built-in retry, for one thing appearing eventually
- waitFor(callback) — general-purpose retry for arbitrary/multiple assertions together
- Modern render/fireEvent/userEvent auto-wrap in act() — explicit act() is the exception now, not the rule
- Forgetting await on findBy/waitFor produces flaky tests, not clear errors — the same risk class as Chapter 4's userEvent gotcha
- Next chapter: Mocking Dependencies — mocking fetch/axios, Mock Service Worker (MSW), timers, and context providers