React120 min total · 17 parts
React Fundamentals: Components, Hooks, and the Virtual DOM
Part 17 of 17 · ~3 min
Common Mistakes Worth Remembering
Every one of these is a bug this dashboard had at some point in this reference. The chapter reference is where it was taken apart.
- Reading state to compute the next state, in a loop or across several updates. Our bulk-reject counter went up by one instead of three. Use
setCount(c => c + 1)whenever the new value depends on the old one. (Chapter 3) - A closure gone stale inside an effect, because some value it depends on never made it into the dependency array. Our autosave overwrote four hundred words of notes with an empty string every ten seconds for exactly this reason. Reach for a ref or the functional updater if you genuinely don't want the effect re-running — muting the lint warning only postpones the bug. (Chapter 7)
- Mutating state or props in place.
candidate.stage = "rejected"oritems.push(x)changes the data without changing the reference, React's comparison sees nothing new, and no re-render happens — until an unrelated render makes the change appear, at an unpredictable moment. Always produce a new array or object. (Chapters 2 and 6) - Missing keys, or the array index as a key on a list that can be sorted, filtered or edited. Our half-typed note followed position instead of person and landed on the wrong candidate's record. This leaks state, not only visuals. (Chapter 4)
- A hook below a conditional return. The early return added during a refactor is the version that actually happens, and it shifts every hook after it into the wrong slot. (Chapter 12)
{value && <Thing />}wherevaluecan be0. Our zero-scoring candidate got a bare0in their score cell. Compare explicitly. (Chapter 1)- Copying a prop into
useStateand expecting it to follow. The review form kept showing the previous candidate's notes. Use the prop, or reset the component with akey. (Chapter 3) React.memoon a child that receives a fresh function or object literal every render. The comparison can never succeed, so you paid for a comparison and skipped nothing. (Chapter 9)- One large context holding unrelated values. Typing in the filter box re-rendered eight hundred rows that only wanted the reviewer's name. Split by how often each value changes, and memoize the provider value. (Chapter 11)
- No cleanup on a subscription. Ten stacked
keydownlisteners from ten opened drawers, all still firing. If the effect starts it, the cleanup stops it — and Strict Mode's double-mount in development will tell you about it immediately. (Chapter 7) - Memoizing everywhere by default, on renders nobody measured. Every dependency array is a correctness liability you now maintain forever. Measure, then optimise. (Chapters 9 and 15)
The same bugs, in a different application
One caution about a reference built around a single running example: it is possible to learn "the dashboard's key bug" rather than "the key bug". So here is the identical mechanism somewhere with no table, no candidates and no drawer — a chat panel:
{messages.map((message, index) => (
<Message key={index} message={message} /> // same bug, new clothes
))}
A new message arrives at the top of the list, every index shifts by one, and React matches by position. The "show original translation" toggle a reader had open stays on row 3 and is now attached to somebody else's message. No table anywhere, same reconciliation rule, same consequence.
Or the stale closure, in an analytics widget with no forms in sight:
useEffect(() => {
const id = setInterval(() => sendHeartbeat(activeTab), 30000);
return () => clearInterval(id);
}, []); // reports "overview" forever, whatever tab is open
If those two read as obvious, the mechanisms transferred, which was the point of building one thing rather than twenty.
Most production React is written in TypeScript, which turns a good share of the prop mistakes above into compile errors — see TypeScript Fundamentals for that side of the same ecosystem. And the patterns here reward being typed out rather than read: state, effects and the closures they capture are all runnable as exercises in the code lab.
Practice this
UI Challenges
Continue learning
- Interview & Career PrepThe Non-Technical Half of the Interview: Behavioral Questions, the STAR Method, and What Recruiters Are Actually Scoring
- AI & LLM EngineeringAI & LLM Engineering Fundamentals: Prompting, RAG, Embeddings, and Function Calling
- TypeScriptTypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime