How do you debug React applications?
TL;DR
Use React Developer Tools to inspect the component tree and profile commits, browser breakpoints and network tools for runtime behavior, and development Strict Mode to expose impure rendering and missing cleanup. Owner stacks help trace which component created failing JSX. React 19.2 also adds React tracks to Chrome performance profiles for scheduler and component work. Use error boundaries for render failures, while handling event and asynchronous errors at their source.
How do you debug React applications?
Effective React debugging combines component-aware tools with ordinary browser runtime, network, and performance inspection.
Using React Developer Tools
The React Developer Tools browser extension is the primary tool for inspecting and debugging React apps. The two tabs:
- Components — inspect the component tree, view/edit props and state and hooks live, jump to the component's source, and use the "Log to console" / "View source" buttons. Toggle "Highlight updates when components render" to spot wasted re-renders visually.
- Profiler — record an interaction and see a flamegraph of every commit, which components rendered, how long each took, and why each rendered (props changed, state changed, hooks changed, parent re-rendered). Indispensable for diagnosing performance issues.
Follow the official React Developer Tools guide for current browser-extension and standalone installation links.
Owner stacks (React 19.1)
React 19.1 introduced owner stacks, which describe the components that created a failing element rather than every component through which it later rendered. In development, captureOwnerStack() can capture this information during render, Effects, React event handlers, and React error handlers. It returns null outside supported React-controlled execution and is unavailable in production builds.
React Performance Tracks (React 19.2)
React 19.2 adds Scheduler and Components tracks to Chrome DevTools performance profiles. They show update priorities, renders, effects, blocked work, and the relationship between React work and browser tasks. Use them when the React Profiler identifies a slow interaction but you need to correlate it with the rest of the page's performance timeline.
Strict Mode
Wrap your tree in <StrictMode> during development to surface common bugs early. React re-renders components an extra time, re-runs Effects through an extra setup/cleanup cycle, and re-runs ref callbacks so you notice impure renders and missing cleanup before they ship. These checks do not run in production.
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMode>,);
Logging and breakpoints
Plain console.log still works, but a few React-specific habits make it more useful:
- Right-click a component in the DevTools Components tab and pick Log component data to console to dump its props, state, and hooks without editing source.
- Use
debuggerstatements inside a render or effect — DevTools pauses there and you can inspect closures and hook call order. - In Chrome DevTools, enable "Pause on uncaught exceptions" and "Pause on caught exceptions" when chasing an error you can't reproduce reliably.
- For tracking why a component re-renders, prefer the DevTools Profiler over hand-rolled logs. The third-party
why-did-you-renderlibrary can add diagnostics when the built-in tools are insufficient. React Compiler may reduce prop-driven re-renders when enabled, but it does not make profiling unnecessary.
Using error boundaries
Error boundaries are React components that catch JavaScript errors in their child component tree. You can implement error boundaries in two ways:
Using React's built-in class component
React's built-in error-boundary API is implemented with class lifecycle methods:
import { Component } from 'react';class ErrorBoundary extends Component {constructor(props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error) {// Update state so the next render will show the fallback UI.return { hasError: true };}componentDidCatch(error, info) {// You can also log the error to an error reporting serviceconsole.error('Error caught by error boundary:', error, info);}render() {if (this.state.hasError) {// You can render any custom fallback UIreturn <h1>Something went wrong.</h1>;}return this.props.children;}}// Usagefunction App() {return (<ErrorBoundary><MyComponent /></ErrorBoundary>);}
Using the react-error-boundary package
Alternatively, you can use the react-error-boundary package for a more convenient approach:
import { useState } from 'react';import { ErrorBoundary } from 'react-error-boundary';import { reportError } from './error-reporting';function ErrorFallback({ error, resetErrorBoundary }) {return (<div role="alert"><p>Something went wrong:</p><pre style={{ color: 'red' }}>{error.message}</pre><button onClick={resetErrorBoundary}>Try again</button></div>);}function App() {const [retryKey, setRetryKey] = useState(0);return (<ErrorBoundaryFallbackComponent={ErrorFallback}onReset={() => setRetryKey((key) => key + 1)}onError={reportError}><MyComponent key={retryKey} /></ErrorBoundary>);}
For handling errors in event handlers or async code, you can use the useErrorBoundary hook:
import { useErrorBoundary } from 'react-error-boundary';import { saveChanges } from './api';function MyComponent() {const { showBoundary } = useErrorBoundary();const handleAsyncError = async () => {try {await saveChanges();} catch (error) {showBoundary(error);}};return <button onClick={handleAsyncError}>Save changes</button>;}
Further reading
- React Developer Tools
- React Docs:
<StrictMode> - React Docs:
captureOwnerStack - React Performance Tracks
- Error boundaries in React
react-error-boundary