![How to Debug ReactJS: A Complete Guide to Faster Issue Fixing [2026] 1 A guide infographic illustrating essential ReactJS debugging tools, techniques, performance profiling, and workflows to identify and fix bugs faster.](https://www.tftus.com/blog/wp-content/uploads/2026/06/feature-12.jpg)
Every React developer will encounter issues and confusing bugs sooner or later — a component that renders the wrong data, a hook that fires in an infinite loop, an error that only shows up in production. The fastest way to debug a React app is to combine React Developer Tools for inspecting component state, Chrome DevTools breakpoints for pausing code execution, and Visual Studio Code’s built-in debugger for stepping through source in your editor — moving past console.log statements saves hours over a project’s lifetime.
According to the Stack Overflow 2025 Developer Survey, over 49,000 developers reported on their workflows and tooling preferences, underscoring how central good debugging practice is to modern React development. The ecosystem has also shifted recently: the State of React 2025-2026 survey — 3,760 responses collected between November 2025 and January 2026 — found 48.4% of daily React users already on React 19, and confirmed that Create React App has been formally sunset, with Vite now the default way most developers scaffold new projects.
Quick-start checklist (about 10 minutes total):
- Install React Developer Tools from the Chrome Web Store — 2 minutes
- Confirm source maps are working (open Sources tab, check you see your real
.jsxfiles, not a bundled blob) — 1 minute - Set your first breakpoint in the Sources tab and reload the page to confirm it pauses there — 2 minutes
<details> <summary><strong>New to React? Quick glossary</strong> (skip if you already know these)</summary>
- Props — data passed into a component from its parent, read-only from the component’s side
- State — a component’s own internal data that can change over time and triggers a re-render when it does
- Hooks — functions like
useStateanduseEffectthat let function components use state and lifecycle features - Source maps — files that connect the bundled/minified code the browser runs back to your original source, so DevTools can show you the real file
- Virtual DOM — React’s in-memory representation of the UI, which it compares between renders to figure out the minimal real-DOM changes needed
</details>
Why Is Debugging React Applications Different?
Debugging React isn’t the same as debugging plain JavaScript. React renders through a virtual DOM, batches state updates, and runs hooks with their own lifecycle — so a stack trace often points at React’s internals rather than your code, and a component can re-render for reasons that aren’t visible in the code you’re looking at. You need to watch the component tree and the data flowing through it, not just the line where the error surfaced. None of this replaces solid unit tests — it’s what you reach for when a bug only shows up at runtime, not in a test file.
Signs you need component-tree debugging, not plain JS debugging:
- The stack trace points into
react-dom.development.jsrather than your own file → use the Components tab, not the raw stack - The bug only appears after a re-render, not on first load → check the Profiler’s “why did this render” data
- The data looks right in
console.logbut wrong on screen → the bug is in JSX/rendering logic, not the value itself
That last one trips people up coming from plain JavaScript: in vanilla JS, console.log(user) shows you the value at that exact moment — nothing more. In React, that same console.log inside a render can show stale data, because the component re-rendered after you logged it but before you looked at the console. The console tells you what the value was on some past render, not necessarily what’s on screen right now — which is exactly why React DevTools’ live-editable Components tab, not console.log, is the tool for confirming current state.
As one developer put it: <blockquote>”if we have created a bug, then we are not in the position to be able to solve it, because if we could, we wouldn’t have created it in the first place. This is why we need additional tools that can help us to step outside ourselves in the process of finding a bug.”<br>— <a href=”https://dev.to/colocodes/how-to-debug-a-react-app-5114″>CoLo Codes, Software Developer, DEV Community</a></blockquote>
That’s the whole case for a real debugging toolkit instead of scattered console.log calls.
React Developer Tools (Browser Extension)
React Developer Tools adds two tabs to your browser’s developer tools: Components and Profiler. Search the Chrome Web Store and select React Developer Tools to install it, then open any React app and press Ctrl+Shift+I (Windows/Linux) or Cmd+Option+I (Mac).
The extension icon gives instant feedback: red means a production build, orange means a development build (dev mode), and no color means the page isn’t running React.
The Components tab displays every component on the page as a tree. Click one to see its props, state, and hooks — and edit them live to see the component update instantly. For large apps with hundreds of components, use the search box to jump straight to a component by name instead of manually expanding the tree.
New to React? This 90-minute crash course covers the components, hooks, and state fundamentals that everything below assumes.
Breakpoints in Chrome DevTools
A breakpoint pauses code execution at a specific line so you can inspect variables and the call stack. In the Sources tab, find your file and click the line number — a blue dot marks the breakpoint, and the code pauses there the next time it runs.
Source maps matter here: a modern build — whether Vite’s dev server or Create React App’s webpack dev server — generates them automatically in development mode, so DevTools shows your actual source file instead of the bundled, minified code.
Power-user tip: right-click a line number instead of clicking it to set a conditional breakpoint — Chrome only pauses when your expression is true. For a list rendering bug, that means pausing only on the one broken item instead of stepping through every render: user.id === 42 pauses only when that specific user renders, not the other 49.
The Debugger Statement
The debugger; statement sets a breakpoint programmatically — insert it anywhere in your code, and the browser pauses there whenever DevTools is open:
function calculateTotal(items) {
debugger;
return items.reduce((sum, item) => sum + item.price, 0);
}
This is especially useful when a file is hard to navigate to in the Sources tree, or when the code you need to pause lives inside a callback or async function. Always remove debugger; statements before shipping to production — they’ll hang the browser for anyone who opens DevTools.
A React-specific place to reach for it: inside a useEffect cleanup function, where a regular breakpoint is easy to miss because it only fires on unmount.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => {
debugger; // confirm cleanup actually runs before you chase a "timer never stops" bug
clearInterval(id);
};
}, []);
Breakpoints vs. Debugger Statement: Which to Use
Both pause code, for different jobs — and there are more variants of each than the two covered above.
| Technique | Best for | Trade-off |
|---|---|---|
| Line breakpoint | File is easy to find, quick temporary pause | Lost when you close the browser |
| Conditional breakpoint | Pausing only on a specific case (one bad list item, one user ID) | Slightly slower than an unconditional pause |
| XHR/fetch breakpoint | Pausing whenever a specific network request fires | Sources tab only — doesn’t cover fetch calls in older Chrome versions |
| Event listener breakpoint | Pausing on a category of DOM event (click, keydown) across the whole page | Can fire on unrelated elements if too broad |
debugger; statement | Code buried in a callback or hard-to-navigate module, or a reproducible pause you want to commit and share | Must remember to remove it before shipping |
VS Code Debugging Setup
Visual Studio Code (VS Code) has a built-in debugger, so you can set breakpoints without leaving your editor.
Create a launch configuration: open the debug panel (Ctrl+Shift+D), select “create a launch.json file,” and choose Chrome. You can reach the same panel any time by clicking the debug icon (sometimes labeled the debug button) in the Activity Bar. Point the URL at your dev server — copy whichever config matches your setup:
Vite:
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome (Vite)",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
}
Create React App:
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome (CRA)",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/src"
}
Start debugging: after running npm run dev (or npm start on Create React App), either click the Run and Debug button in the sidebar or press F5. VS Code launches Chrome and attaches the debugger — you should see the debug icon light up once it connects. Click line numbers to set breakpoints; when execution reaches one, the panel on the left shows every variable in scope. The debug toolbar that appears lets you continue, step over, step into, or step out.
Reading the Call Stack and Profiling Performance
The call stack lists every one of the functions that led to the current line — read it top-down, and the first line that isn’t part of React’s internals is usually the root cause. By default the Sources panel hides React’s own stack frames; you can turn them back on if you need the full picture.
The Profiler tab — React’s built-in profiler tool — shows what rendered and how long each component took. Click record, interact with the slow part of your app, then stop — the flame chart highlights the biggest and most-repeated bars first, and tells you why each component rendered (a prop change, a state change, or a context change). <blockquote>”The performance of React applications depends heavily on JavaScript bundle size because it determines both initial load times and Time to Interactive (TTI) as well as user experience (UX) and search engine rankings.”<br>— <a href=”https://www.forbes.com/councils/forbestechcouncil/2025/08/25/beyond-the-bundle-strategic-javascript-optimization-for-high-performance-react-apps/”>Raju Dandigam, Engineering Manager, Navan</a></blockquote>
Don’t guess at what’s slow — DebugBear’s guide to measuring React performance confirms that effective optimization starts with measurement using the browser’s own developer tools to spot unnecessary re-renders before you touch any code.
Debugging React Hooks
Hooks introduce a class of bugs that don’t show up in the Components tab at all: stale closures, where a function captures an old value of state or props and keeps using it after a re-render; infinite useEffect loops, usually caused by a dependency that changes identity on every render; and dependency-array mistakes in general.
A stale closure in practice — this counter always logs 0, no matter how many times you click, because the interval callback closed over the count from the first render and never sees a newer one:
// Before — stale closure, logs 0 forever
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, []); // count is missing from the dependency array
// After — add count as a dependency so the closure stays current
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, [count]);
<blockquote>”Understanding React’s Hook system and life cycle is essential to overcome common obstacles such as maintaining state consistency and managing any side effects properly.”<br>— <a href=”https://www.forbes.com/councils/forbestechcouncil/2024/07/29/understanding-react-hooks-and-how-to-use-them/”>Nick Damoulakis, President, Orases</a></blockquote>
The most common culprit is an inline function or object passed into a useEffect, useMemo, or useCallback dependency array — it gets a new reference on every render, so React thinks the dependency changed every single time, even when the underlying data didn’t. Here’s an illustrative version of that exact pattern: a search results page rendering each of 50 result cards six times on initial load, because a useEffect dependency array was missing a memoized callback, adding up to 300 unnecessary renders — a 2,400ms total render time in the Profiler is what would make a 6x multiplier obvious at a glance.
Community reports on Stack Overflow describe a related pattern: developers debugging a component that renders multiple times for no clear reason have had to set breakpoints 15–16 times just to trace two dropdowns re-rendering each other — a sign that the fix usually isn’t more breakpoints, it’s React.memo, useMemo, or useCallback applied at the right layer.
Debugging Network and Async Operations
A large share of “the data is undefined” bugs aren’t React bugs at all — they’re failed API calls, malformed responses, or CORS errors that never make it into component state. Before spending time in the Components tab, open the Network tab and check the actual request: status code, response body, and timing.
Race conditions are the other common async trap — two requests firing close together, with the slower one resolving last and overwriting fresher state. Watch for this whenever a component fetches on every keystroke or every prop change; if the fetch doesn’t cancel or ignore stale responses, the UI can briefly show the wrong data even though every individual request succeeded.
Cancel the stale request instead of racing it, with AbortController in a useEffect cleanup:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort(); // cancels this fetch if query changes again first
}, [query]);
A related but subtler version of this is setState timing rather than a race between two requests: calling setState inside an async callback after the component has already unmounted (the user navigated away while the fetch was in flight) triggers React’s “can’t perform a state update on an unmounted component” warning. The fix is the same AbortController pattern above, or a isMounted-style guard flag checked before the setState call inside the .then().
Debugging React Apps in Production
Production debugging requires a different toolkit than local development, because you can’t reproduce the user’s exact state and you can’t attach a breakpoint to someone else’s browser.
Error Boundaries are React components that catch JavaScript errors in their child component tree and render a fallback UI instead of letting the whole app crash — wrap risky sections of your app in one so a single broken component doesn’t take down the page.
Source maps need to be deployed (not just generated) so a production stack trace points at your real source file instead of bundle.js:1:45832. Without them, you’re debugging minified code blind.
Remote error monitoring — a service like Sentry — captures the stack trace, device details, and the sequence of events leading up to a production error, and can accept source maps privately so you get readable stack traces without exposing your source code publicly. A minimal setup is a few lines:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
tracesSampleRate: 0.2, // sample 20% of transactions in production
});
What that looks like as an actual debugging session: open the Profiler, record an interaction, and see the render count first — “opened the Profiler, saw a component re-rendering 300 times on a single search, wrapped it in React.memo, and the count dropped to 50.” That’s the whole workflow — Profiler to spot the number, React.memo/useMemo/useCallback to fix it, Profiler again to confirm.
Two brief-sourced examples of that same discipline applied at production scale: Product Perfect’s engagement with legal-processing platform Simpluris is best read for the debugging pattern, not the headline number — the team traced a manual returns-mail process down to its root inefficiency and refactored it behind proper DevOps CI/CD pipelines, which is what took processing from 4.5 hours to 30 minutes. Bacancy Technology’s work on HarbourLiving’s React UI is the more directly debugging-relevant of the two: the team resolved recurring login connection errors and session-handling bugs specifically by implementing structured error logging, the same “instrument first, then diagnose” approach the Error Boundaries and Sentry setup above are built on.
Common React Debugging Mistakes to Avoid
- Relying only on
console.log. It’s fine for a quick spot-check, but it clutters the codebase fast — use breakpoints for anything more involved. - Skipping the full error message. React’s dev-mode errors are usually specific about what’s wrong; read past the first line.
- Setting breakpoints in compiled code. Without working source maps, your breakpoint lands in minified code, not the file you’re editing.
- Guessing at performance problems. Record with the Profiler tab instead of assuming which component is slow.
- Leaving
debugger;statements in production. As mentioned earlier, search your code base fordebuggerbefore every deploy — one left behind freezes the app for any user with DevTools open.
Common React Error Messages Decoded
| Error | What it usually means | Fix |
|---|---|---|
Cannot read properties of undefined (reading 'x') | You’re accessing a property before the data has loaded | Add a loading state or optional chaining (data?.x) |
Too many re-renders | setState is being called unconditionally during render, not inside an event handler or effect | Move the setState call into a useEffect or an event handler |
Objects are not valid as a React child | You’re rendering an object or array directly in JSX instead of a string/number | Render a specific field ({user.name}) instead of the whole object |
Cannot update a component while rendering a different component | A setState call from one component is firing during another component’s render | Wrap the state update in a useEffect, or trigger it from an event handler instead |
Invalid hook call. Hooks can only be called inside the body of a function component | A hook is called inside a loop, condition, nested function, or a mismatched React version | Move the hook to the top level of the component; check for duplicate React copies in node_modules |
Warning: Each child in a list should have a unique "key" prop | A .map() rendering a list is missing (or reusing) key values | Use a stable, unique ID from your data — not the array index if the list can reorder |
A Quick Decision Tree for React Bugs
Before diving in, four questions narrow down which toolkit you actually need:
- Can you reproduce it locally with React DevTools open? If not, you need production instrumentation first — error boundaries, remote logging, deployed source maps — not better local tools.
- Is it about state, re-renders, or component lifecycle — a value that’s wrong, a component re-rendering too often, or props/state that mutated unexpectedly? Visual debugging with the Components tab and Profiler works well here. If it’s about async timing or race conditions instead, you need the Network tab plus async-aware breakpoints.
- Is a third-party component library involved? Try isolating the issue in a minimal reproduction without the library first — most perceived library bugs are actually prop mismatches or version conflicts.
- Does it only happen in production or a specific browser? Verify source maps are actually deployed and configured — the Sources tab should show your original files, not a bundled blob.
Named quick-reference for the three complaints that show up in almost every debugging session: an infinite loop almost always means a useEffect dependency array problem — open the Profiler first to confirm the render count, then check the effect’s dependencies. Wrong data on screen with correct-looking state means a props-drilling or stale-closure problem — start in the Components tab at the component showing bad data, then walk up the tree. A console warning about props (missing key, invalid prop type) means exactly what it says — fix it at the source component, not the one receiving the warning.
A Real Debugging Session, Start to Finish
Here’s what that decision process looks like end to end, on a stale-closure bug — a plausible, illustrative walkthrough of the pattern, not a specific real incident:
The bug report: a “mark as read” button in a notifications list works the first time, but after that it always marks the first notification as read instead of whichever one was clicked.
- Reproduce it. Confirmed locally — click notification #3, and #1 gets marked instead, every time after the first click.
- Check the data first. Open the Components tab, click the notification list component. Props and state both look correct — the right
idis being passed to each row’s click handler. So the bug isn’t in the data. - That rules out props drilling — this is a logic bug, most likely in the event handler itself. Set a breakpoint inside the
onClickhandler. - Inspect the closure. The breakpoint pauses on every click, and the Variables pane shows the handler is reading
notifications[0].idevery time, regardless of which row was clicked — the handler was defined once, outside the render that had the currentnotificationsarray, and never updated. - The fix: the handler was built with an empty dependency array in a
useCallback, capturing the first render’snotificationspermanently. Addingnotificationsto the dependency array fixes it — a fresh handler gets created each time the list changes, so it always closes over current data. - Confirm: click each notification in turn; each one now marks itself, not #1.
Total tool sequence: Components tab to rule out bad data → breakpoint plus Variables pane to catch the stale closure in the act → dependency array fix → manual re-test to confirm. No profiling needed here, because the symptom was wrong behavior, not slow rendering.
Quick Reference: Debugging Tools for React
| Tool | What It Does | When to Reach For It | Setup Time |
|---|---|---|---|
| React Developer Tools | Inspect component tree, props, state, hooks | Any time a component renders wrong data | ~2 min install, free |
| Chrome DevTools Sources | Set breakpoints, step through code, read call stack | Logic errors and unexpected code paths | Built in, free |
| VS Code Debugger | Debug inside your editor with breakpoints | Preferred workflow for developers who live in VS Code | ~5 min (launch.json), free |
| Profiler Tab | Measure render times, identify slow components | Performance issues and unnecessary re-renders | Built in, free |
| Network Tab | Inspect requests, responses, timing | “Undefined” data and async/race-condition bugs | Built in, free |
| Error Boundaries | Catch child-tree errors, render fallback UI | Preventing a single broken component from crashing the app | ~15 min to write one |
| Sentry (or similar) | Capture and alert on production errors | Any production React application | ~15–30 min integration; free tier covers 5K errors/month |
Frequently Asked Questions
Q1. How do I debug ReactJS in VS Code?
Install the Chrome debugger integration by creating a .vscode/launch.json file with a Chrome launch configuration pointed at your dev server’s URL. Start your app (npm run dev for Vite, npm start for Create React App), then press F5 or click Run and Debug. Click a line number in VS Code to set a breakpoint — execution pauses there, and the sidebar shows every variable in scope.
Q2. How do I debug React JS in Chrome?
Install the React Developer Tools browser extension, then open your app and press Ctrl+Shift+I (Cmd+Option+I on Mac). Use the Components tab to inspect props and state, the Sources tab to set breakpoints, and the Console tab to read error messages and stack traces.
Q3. How do I inspect React component props and state in Chrome?
Open the Components tab in React Developer Tools and click any component in the tree. Its current props, state, and hooks appear in the panel on the right — you can edit values directly and watch the component re-render live, which is the fastest way to confirm whether a bug is a data problem or a logic problem.
Q4. How do I debug React rendering issues?
Start with the data: inspect the component’s props and state in React Developer Tools. If the incoming data is already wrong, the bug is upstream — look at the parent component or the API call feeding it. If the data is correct but the output isn’t, the bug is in that component’s own rendering logic.
Q5. How do I identify why my React component is re-rendering?
Open the Profiler tab, click record, interact with the app, then stop. The Profiler tells you why each component rendered — a prop change, a state change, or a context change — and the flame chart highlights which components rendered the most or took the longest, so you’re not guessing.
Q6. How do I debug React hooks like useEffect?
Check the dependency array first — an inline function or object recreated on every render will make React think the dependency changed every time, which causes infinite loops or effects that run more than expected. Use the Profiler’s “why did this render” data to confirm, then memoize the offending value with useMemo or useCallback.

Leave a Reply