Runnable lesson · press Try it on the examples to edit and run them here.
Debugging is a skill with a method. React errors look scary, but they are usually precise. This lesson gives you a routine and the tools.
Step 1: read the error, from the top
React and the browser tell you a lot:
- The message: “Cannot read properties of undefined (reading ‘name’)” means you used
x.namewhilexwasundefined. - The file and line: click it in the console to open the code.
- The component stack React adds (“The above error occurred in the
<ProductCard>component”). It tells you which component to look at. - Warnings: missing keys, invalid DOM attributes, controlled/uncontrolled inputs, state updates in render. They are bugs waiting to happen. Fix them all.
Fix the first error first. Later ones are often just consequences.
Step 2: look at the data
Most React bugs are “the data is not what I think”. Show it:
function App() {
const [id, setId] = React.useState(1);
const user = { id, name: "Ada", roles: ["admin", "editor"] };
// 1. console.log shows the values in the console panel
console.log("render with", user);
// 2. console.table is nicer for arrays of objects
console.table([{ id, name: "Ada" }, { id: id + 1, name: "Grace" }]);
return (
<div>
<h3>Debug view</h3>
{/* 3. Show an object on the page while developing */}
<pre>{JSON.stringify(user, null, 2)}</pre>
<p>id={id}</p>
<button onClick={() => setId(id + 1)}>Next id</button>
</div>
);
}
Tips:
- Put the
console.logat the top of the component to see every render and the props and state it received. - Label your logs:
console.log("user:", user), notconsole.log(user). - Remove debug logs before you commit.
JSON.stringifyon the page is a quick way to see state without the console.
Step 3: React Developer Tools
The browser extension adds two tabs to the developer tools:
- Components: the tree of your components. Click one to see its props, state and hooks, and even edit them live to test behaviour. Use the search box to find a component and the “inspect” button to pick one from the page.
- Profiler: record an interaction to see which components rendered and how long each took. Turn on “Highlight updates when components render” in the settings to see re-renders as flashing outlines.
Ask: are the props and state what I expect? If yes, the bug is in the rendering logic. If no, find where they were set wrong, going up the tree.
Step 4: breakpoints
A breakpoint pauses the program so you can inspect every variable. Open the Sources panel in the browser developer tools, find your file (source maps show the original code), and click a line number. Or write debugger; in the code: when developer tools are open, the page stops there. Then step through line by line and watch variables (Debugger and Breakpoints). It beats adding ten console.log calls.
Step 5: the Network panel
For data problems, open the Network tab: did the request go out? To which URL? What status came back? What did the response body contain? Many “my component is empty” bugs are a 404, a 500 or a different JSON shape (Network Panel).
Common symptoms and likely causes
| Symptom | Likely cause |
|---|---|
| Screen does not update after a click | Mutated state instead of setting a new value (Avoiding State Mutation) |
| Handler sees an old value | Stale closure or snapshot (State as a Snapshot) |
| Infinite loop, “Too many re-renders” | Setting state during render; effect that sets its own dependency |
| “Cannot read properties of undefined” | Data not loaded yet, or the shape differs; use optional chaining and loading states |
| Input loses focus on every keystroke | A component defined inside another component (Common Rendering Mistakes) |
| Effect runs twice | Strict Mode, on purpose; fix cleanup (Strict Mode) |
| List items swap state | Missing or index keys (Keys) |
| Blank page | An uncaught render error (read the console), or a wrong path in production |
| Works in dev, breaks in production | Wrong base, missing env variable, case-sensitive file names (Common Production Errors) |
A bug to find
This counter is supposed to add one per click, but nothing happens. Run it, press the button and check the console panel and the screen. Then fix it.
function App() {
const [items, setItems] = React.useState([]);
function add() {
items.push("x"); // what is wrong here?
setItems(items);
}
return (
<div>
<p>Count: {items.length}</p>
<button onClick={add}>Add</button>
</div>
);
}
The array is mutated and then given back to React as the same reference, so React sees no change. The fix is setItems([...items, "x"]):
function App() {
const [items, setItems] = React.useState([]);
function add() {
setItems([...items, "x"]);
}
return (
<div>
<p>Count: {items.length}</p>
<button onClick={add}>Add</button>
</div>
);
}
A debugging routine
- Reproduce the bug and find the smallest steps that trigger it.
- Read the first error and the component stack.
- Check the data (props, state, network response).
- Form a guess, and test it with a log or a breakpoint.
- Fix one thing at a time.
- Add a test so it cannot come back (Basic Testing Concepts).
When you are stuck, make a minimal example in the playground: removing everything that is not part of the problem often reveals the cause, and gives you something small to ask about.
Key points
- Read the first error and the component stack; fix warnings too.
- Inspect data with logs, React DevTools, breakpoints and the Network panel.
- Match the symptom to the usual cause, and reduce the problem to a minimal example.