Quick Answer
How I Cut My Debugging Time in Half as a React After tracking my debugging sessions for a month, I discovered that 80% of my bugs fell into 5 categories.
How I Cut My Debugging Time in Half as a React Developer
Last month, I started timing my debugging sessions. Not because I'm obsessive—because I was frustrated.
I'd spend hours chasing bugs that, in hindsight, had obvious causes. After tracking 47 debugging sessions, a pattern emerged:
80% of my bugs fell into just 5 categories.
Once I recognized this, I changed my approach. Here's what I learned.
The 5 Bug Categories (And What Fixed Them)#
1. State Wasn't What I Thought It Was (32% of bugs)#
The most common bug: I assumed state had a certain value, but it didn't.
Before: Console.log everywhere, guessing what went wrong.
After: React DevTools + this mental checklist:
- When was this state last updated?
- What triggered that update?
- Is there a race condition?
The fix that helped most: Adding TypeScript strict mode. Half of these bugs became compile errors instead of runtime mysteries.
// This catches so many bugs
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
2. useEffect Running When It Shouldn't (24% of bugs)#
Infinite loops. Stale closures. Effects firing on every render.
Before: Adding and removing dependencies randomly until it worked.
After: I started asking two questions:
- "What external system am I syncing with?"
- "If the answer is 'nothing,' do I even need useEffect?"
The React docs page "You Might Not Need an Effect" should be required reading. Seriously.
Most impactful change: Moving derived state to render instead of useEffect:
// Before (buggy)
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// After (just works)
const fullName = `${firstName} ${lastName}`;
3. Async Timing Issues (18% of bugs)#
Data loads, but the component already unmounted. Or state updates in the wrong order.
Before: Sprinkling setTimeout and hoping for the best.
After: Three rules I now follow:
- Always handle loading states explicitly
- Use React Query or SWR instead of raw fetch
- Check if component is mounted before updating state
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, []);
4. Props Drilling Gone Wrong (15% of bugs)#
Components receiving undefined because something in the chain didn't pass the prop.
Before: Adding console.logs at every level until I found the gap.
After:
- TypeScript catches most of these at compile time
- Context for truly global state (but sparingly)
- Component colocation—keeping state close to where it's used
The mindset shift: If I'm drilling more than 2 levels, I'm probably structuring something wrong.
5. CSS/Layout Bugs (11% of bugs)#
Not JavaScript, but they still ate my time.
Before: Randomly changing Tailwind classes until it looked right.
After:
- Browser DevTools → Computed tab to see actual values
outline: 1px solid redon parent containers to visualize layout- Understanding that 90% of layout bugs are flexbox or grid misunderstandings
The Tools That Actually Made a Difference#
After trying dozens of debugging tools, here's what I actually use:
React DevTools#
Free, essential, criminally underused. The Components tab shows real-time props and state. The Profiler shows what's re-rendering and why.
TypeScript Strict Mode#
Catches so many bugs at compile time. The initial migration is painful, but worth it.
React Query / TanStack Query#
Handles caching, loading states, and refetching. Eliminated most of my async bugs.
Console.table()#
Better than console.log for arrays and objects:
console.table(users); // Actually readable
Cursor / AI Assistant#
I pipe error messages directly to AI now. Not for blind copying, but for "what should I be looking for?" It's like having a rubber duck that talks back.
The Real Change: Process, Not Tools#
Tools helped, but the bigger change was process.
I now spend 5 minutes understanding before touching code:
- What did I expect to happen?
- What actually happened?
- What's the smallest test case that reproduces this?
This sounds obvious, but I used to skip straight to "let me try this" mode. That mode is expensive.
The Numbers#
After a month of tracking:
| Metric | Before | After |
|---|---|---|
| Avg debugging time | 34 min | 16 min |
| Bugs fixed first try | 23% | 58% |
| "What the hell" moments | Many | Fewer |
The 50% time reduction is nice, but the real win is fewer "I have no idea what's happening" moments. Those are exhausting.
What I'm Still Bad At#
For transparency:
- Performance bugs — I still struggle with knowing when to reach for useMemo
- Race conditions — Better, but not solved
- CSS grid — We have a complicated relationship
The goal isn't perfection. It's fewer wasted hours.
What debugging patterns do you see in your work? I'm curious if the 5 categories match your experience—let me know.