Module 8: Tooling and Testing
Debugging React: DevTools, Error Messages and a Method
How to use React DevTools, what the common error messages are really telling you, and a repeatable way to narrow a bug down.
Beginners debug by changing things until the bug goes away. That works about half the time and teaches you nothing. A better habit is to ask a narrow question, use the right tool to answer it, and only then change code.
React DevTools#
Install the React Developer Tools extension for Chrome, Edge or Firefox. Once it's installed, two new tabs appear in browser DevTools on pages that use React: Components and Profiler.
Components tab#
You see the component tree, not the DOM. Click any component and the right-hand panel shows its props, its hooks (every useState, useRef, useContext and so on, with current values), and where it was rendered from.
The part I use constantly: you can edit props and state values right there. Want to see what the empty state looks like? Change items to [] in the panel and the UI updates. No code change, no reload.
Components
▾ App
▾ Layout
Header
▾ ProductList
▸ ProductCard key="12"
▸ ProductCard key="13"
Selected: ProductCard
props: product: {id: 12, name: "Pen"} onAdd: ƒ
hooks: State: false
Context: {theme: "dark"}
Also handy: the search box (find a component by name), the eye icon (jump to the DOM node), the bug icon (log the component's data to the console), and the <> icon (jump to the source). Components you named in lowercase or anonymous arrow functions assigned inline show up as "Anonymous", which is a good reason to use named function components.
Seeing why something re-rendered#
In the Profiler settings, turn on "Record why each component rendered while profiling." Then record an interaction. The flame graph shows each component rendered, how long it took, and the reason: "props changed: onSelect", "hook 2 changed", "parent rendered". That one panel answers most performance questions in minutes.
Also turn on Highlight updates when components render in the Components settings. Colored outlines flash around anything that renders, so you can literally watch what's firing when you type.
StrictMode is your friend#
We've mentioned that StrictMode double-invokes renders and effects in development. Treat anything it reveals as a real bug in your code. A counter that jumps by two, an effect that subscribes twice, a log that prints twice: each of those is pointing at an impure render or a missing cleanup. Don't fight it.
Reading the common errors#
React's messages are better than they used to be. Here's what the frequent ones mean.
"Too many re-renders. React limits the number of renders to prevent an infinite loop."
You set state during render. Almost always this is onClick={setCount(1)} instead of onClick={() => setCount(1)}, or an unconditional setX() call in the function body.
"Each child in a list should have a unique 'key' prop."
Your map doesn't put a stable key on the returned elements. Check that it's on the outermost element in the callback.
"Rendered more hooks than during the previous render." / "Rendered fewer hooks than expected."
A hook ran conditionally, or after an early return. Move all hook calls above any if (...) return.
"Objects are not valid as a React child (found: object with keys {...})."
You put an object in JSX braces, like {user} instead of {user.name}. Or a Date. Or a promise (forgot await).
"Cannot update a component while rendering a different component." During the render of component A, you called a setter that belongs to component B. Render must be pure. Move the update into an effect or an event handler.
"A component is changing an uncontrolled input to be controlled."
The value started as undefined and later got a real value. Initialize with an empty string.
"Maximum update depth exceeded." An effect sets state on every run and the state is in its own dependency list. Check the dependency array and what the effect sets.
"Cannot read properties of undefined (reading 'map')."
Data you expected hasn't arrived yet, so items is undefined on first render. Initialize it as [], or guard for the loading state before rendering the list.
A warning about a missing dependency in useEffect.
Believe it. Fix the cause, don't silence the lint.
Always read the component stack printed under the error. It shows which component threw and who rendered it, so you can jump straight to the file.
Browser tools you shouldn't forget#
- Network tab. Is the request even sent? What was the status? What was the response body? A shocking share of "React bugs" are the server returning something different from what you assumed. Check this before touching code.
- Console.
console.logis fine. Label your logs (console.log("items", items)) so you can tell them apart, and useconsole.tablefor arrays of objects. - Breakpoints. Put
debugger;in a handler or open the Sources tab and click a line number. Execution pauses, and you can inspect every variable in scope and step line by line. This is better than a pile of logs once you're past the simplest issues. Production builds minify code, so source maps matter, and Vite generates them in dev by default. - Application tab. See what's in localStorage and cookies. Stale persisted data causes bugs that vanish in a private window.
ESLint as a debugging tool#
The react-hooks ESLint plugin that the Vite template installs catches hooks-order mistakes and missing dependencies as you type. A red squiggle there is cheaper than an afternoon of confusion. Don't turn rules off to make the warnings go away.
A method for narrowing a bug#
When something's wrong and you don't know why, I work through these questions in order.
- Can I reproduce it reliably? If not, find the steps first. Write them down.
- Is the data right? Check the props and state in DevTools. Is the component receiving what you think it is?
- If the data is wrong, where did it go wrong? Trace upward. Whoever passed this prop, whoever set this state. Log or breakpoint at each step until you find the first place the value differs from what you expected.
- If the data is right but the UI is wrong, the bug is in the render logic. Read the JSX and the conditions.
- Is it a timing problem? Stale closure, race condition, effect that runs at the wrong moment. Log the order of events with timestamps.
- Can I shrink it? Remove things until the bug disappears, then put back the last thing. Or copy the component into a small isolated test page.
The key is that each step splits the problem in half. Randomly editing code never narrows anything.
Where people get burned#
- Debugging in production builds with minified names and no source maps.
- Using
console.log(state)right after asetStateand concluding the setter doesn't work (it's the snapshot rule). - Ignoring warnings because "the app still works".
- Looking at the code when the problem is the network response.
- Changing five things at once, so you can't tell which one fixed it.
- Logging an object, then expanding it later in the console and seeing its current value rather than the value at log time.
Practice#
- Install React DevTools, open any app you've built, and change a state value from the Components panel to see the UI react.
- Turn on "highlight updates" and type in an input. Which components flash? Is that what you expected?
- Deliberately cause three errors from the list above (an infinite render loop, a missing key, a conditional hook) and read what each message says before fixing it.
- Record a Profiler session for a list that re-renders every time you click one item, and read the "why did this render" panel.