
React Errors Debugging
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-errors-debugging is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-errors-debugging
- Frontend Development
- AI-coding skill
React Errors Debugging by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-errors-debuggingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-errors-debugging
Quick Reference
Debugging Tools Overview
| Tool | Purpose | When to Use |
|---|---|---|
| React DevTools (Components) | Inspect props, state, hooks, context | Component data inspection |
| React DevTools (Profiler) | Render timing, flamegraph, ranked chart | Performance investigation |
| Browser DevTools (Console) | Read React warnings and error messages | Error diagnosis |
| Browser DevTools (Sources) | Set breakpoints, step through code | Logic debugging |
<StrictMode> | Surface impure renders, unsafe effects | Development-time bug prevention |
why-did-you-render | Log unnecessary re-renders with reasons | Re-render optimization |
| Source maps | Map production errors to original source | Production debugging |
Critical Warnings
NEVER remove <StrictMode> to "fix" double-rendering issues -- the double invocation is intentional. It exposes impure components and side effects. ALWAYS fix the underlying impurity instead.
NEVER ignore React console warnings -- they indicate real bugs or future breaking changes. ALWAYS resolve every warning before shipping.
NEVER debug production builds without source maps -- error messages are stripped and minified in production. ALWAYS generate source maps for staging/debugging environments.
ALWAYS check the component stack trace when diagnosing errors -- React prints the component tree path that led to the error, which pinpoints the source.
ALWAYS use the Profiler tab (not just the Components tab) when investigating performance -- visual inspection of the component tree does NOT reveal render timing.
---
Strict Mode
<StrictMode> is a development-only tool that helps find bugs by intentionally double-invoking certain functions.
What Strict Mode Double-Invokes
| Function Type | Double-Invoked? | Why |
|---|---|---|
| Component function body | Yes | Detects impure rendering (side effects during render) |
useState initializer | Yes | Detects impure initialization |
useReducer reducer | Yes | Detects impure reducers |
useReducer initializer | Yes | Detects impure initialization |
useMemo callback | Yes | Detects impure memoization |
useEffect setup + cleanup | Yes (mount/unmount/remount) | Detects missing cleanup logic |
useRef — NO | No | Refs are not double-invoked |
Strict Mode Behavior
// StrictMode wraps your app (or a subtree)
import { StrictMode } from "react";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);In development, React will: 1. Render component -> call cleanup -> re-render component (simulating unmount/remount) 2. Call state initializers twice and discard the first result 3. Call reducers twice and discard the first result
Common Strict Mode Mistakes
// BAD: Side effect in render — Strict Mode exposes this by double-calling
function Counter(): JSX.Element {
const [count, setCount] = useState(0);
document.title = `Count: ${count}`; // NEVER do this in render
return <div>{count}</div>;
}
// GOOD: Side effect in useEffect
function Counter(): JSX.Element {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <div>{count}</div>;
}// BAD: Effect without cleanup — Strict Mode exposes the leak
function ChatRoom({ roomId }: { roomId: string }): JSX.Element {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// Missing cleanup! StrictMode will connect TWICE
}, [roomId]);
return <div>Chat: {roomId}</div>;
}
// GOOD: Effect with proper cleanup
function ChatRoom({ roomId }: { roomId: string }): JSX.Element {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect(); // ALWAYS clean up
}, [roomId]);
return <div>Chat: {roomId}</div>;
}---
Console Warnings Diagnostic Table
| Warning Message | Cause | Fix |
|---|---|---|
| "Each child in a list should have a unique 'key' prop" | Array items rendered without key or with duplicate/index keys | ALWAYS provide a stable, unique key from data (e.g., id). NEVER use array index as key when items reorder. |
"Cannot update a component (X) while rendering a different component (Y)" | Calling setState of component X inside the render body of component Y | Move the state update into a useEffect or event handler. NEVER call setState during render of another component. |
| "Maximum update depth exceeded" | Infinite re-render loop from setState in useEffect without proper deps | Check useEffect dependencies — ensure the effect does not unconditionally trigger its own dependency. Add missing deps or restructure logic. |
| "Objects are not valid as a React child" | Passing an object/array directly as JSX child text | Convert to string with JSON.stringify(), or map array to elements. NEVER render raw objects. |
| "Function components cannot be given refs" | Passing ref to a function component without forwardRef | Wrap component with forwardRef (React 18) or accept ref as a prop (React 19). |
| "Can't perform a React state update on an unmounted component" | setState called after component unmounted (React 17 and earlier) | This warning was REMOVED in React 18. If seen in React 18+, you have a version mismatch. In React 17, use cleanup to cancel async operations. |
| "Invalid hook call" | Hook called outside component/hook, or mismatched React versions | Verify hooks are at top level of component/hook. Check for duplicate React installations with npm ls react. |
| "Rendered more hooks than during the previous render" | Conditional hook call — hook count changed between renders | NEVER call hooks inside conditions, loops, or early returns. ALWAYS call all hooks in the same order. |
---
React DevTools
Components Tab
The Components tab shows the live component tree with inspectable data:
| Feature | What It Shows | How to Use |
|---|---|---|
| Component tree | Hierarchy of mounted components | Click to select, use search to filter |
| Props panel | Current props of selected component | Expand objects, edit values live |
| Hooks panel | Current hook values (state, ref, memo, context) | Shows hook name and current value |
| Source link | "< >" icon links to component source | Click to open in Sources tab |
| Rendered by | Which parent rendered this component | Shows in sidebar when component selected |
| Owner stack (React 19) | Full owner component chain | Shows which component created this one |
Profiler Tab
| Feature | Purpose | Interpretation |
|---|---|---|
| Flamegraph | Visualize render time per component | Wide bars = slow renders. Grey bars = did not render. |
| Ranked chart | Components sorted by render duration | Focus optimization on top-ranked components |
| "Why did this render?" | Shows render trigger reason | Enable via gear icon -> "Record why each component rendered" |
| Commit selector | Browse individual render commits | Each commit = one React render batch |
| Render duration | Time in ms for each component | Compare before/after optimization |
Profiler Workflow
1. Open React DevTools -> Profiler tab 2. Click gear icon -> enable "Record why each component rendered while profiling" 3. Click Record (blue circle) 4. Perform the interaction you want to measure 5. Click Stop 6. Analyze: flamegraph for overview, ranked chart for worst offenders 7. Check "Why did this render?" for each slow component
---
Component Stack Traces
When React throws an error, it prints a component stack trace showing the component hierarchy:
Error: Something went wrong
at UserProfile (UserProfile.tsx:15)
at div
at Dashboard (Dashboard.tsx:8)
at ErrorBoundary (ErrorBoundary.tsx:5)
at App (App.tsx:12)Reading Component Stacks
- Read top to bottom: the topmost component is where the error occurred
- Native HTML elements (e.g.,
at div) appear in the stack - File names and line numbers point to the component definition
- ALWAYS look at the first custom component in the stack -- that is the error source
React 19 Improvements
React 19 introduces owner stacks via captureOwnerStack():
import { captureOwnerStack } from "react";
// In an error boundary or error handler:
function handleError(error: Error): void {
const ownerStack = captureOwnerStack();
console.error("Error:", error.message);
console.error("Owner stack:", ownerStack);
// Owner stack shows WHICH component created the erroring component
// (not just the render tree, but the ownership/creation chain)
}---
Common Error Messages Diagnostic Table
| Error Message | Cause | Fix |
|---|---|---|
Minified React error #XXX | Production build with stripped error messages | Look up the error code at https://react.dev/errors/XXX or debug with development build |
Too many re-renders | Component unconditionally calls setState during render | NEVER call setState directly in component body. Use useEffect or event handlers. |
Hydration failed because the initial UI does not match | Server HTML differs from client render | Ensure server and client render identical output. Check for typeof window guards, Date.now(), or random values in render. |
Cannot read properties of null (reading 'useState') | Multiple React copies or hook called outside component | Run npm ls react to check for duplicates. Ensure hooks are called inside components only. |
Uncaught Invariant Violation | Internal React assertion failed | Usually indicates a bug in your code violating React rules. Check the message details. |
Element type is invalid: expected a string or class/function | Component is undefined at render time | Check import statement — likely a typo, default vs named export mismatch, or circular dependency. |
Cannot update during an existing state transition | setState called inside render() or getDerivedStateFromProps | Move state update to useEffect or componentDidMount/componentDidUpdate. |
---
Development vs Production Differences
| Aspect | Development | Production |
|---|---|---|
| Error messages | Full descriptive messages with component stacks | Minified error codes (e.g., Minified React error #310) |
| Strict Mode | Active (double-invokes renders, effects) | Completely stripped — no double invocations |
| Console warnings | All warnings displayed | Most warnings suppressed |
| Performance | Slower due to extra checks, Strict Mode overhead | Optimized, no dev-only checks |
| Profiling | DevTools Profiler works out of the box | Requires profiling build: react-dom/profiling |
| Source maps | Available by default (dev server) | Must be explicitly generated and deployed |
| PropTypes | Validated at runtime (if used) | Stripped entirely |
Debugging Production Errors
1. Look up minified error codes: Visit https://react.dev/errors/{code} for the full message 2. Generate source maps: Configure your bundler to produce .map files for staging 3. Use profiling build for production performance analysis:
// webpack.config.ts — alias for profiling build
resolve: {
alias: {
"react-dom$": "react-dom/profiling",
"scheduler/tracing": "scheduler/tracing-profiling",
},
}4. Error monitoring: Use services that can decode source maps server-side (Sentry, Bugsnag)
---
React 19 Debugging Improvements
| Feature | React 18 | React 19 |
|---|---|---|
| Hydration mismatch errors | Generic "did not match" message | Detailed diff showing exact HTML differences |
| Error reporting | Component stack only | Owner stacks via captureOwnerStack() |
| Third-party script handling | Hydration errors on injected elements | Automatically skips unexpected <script>, <style>, <link> in <head>/<body> |
| ref on function components | Requires forwardRef wrapper | ref is a regular prop — no wrapper needed |
| useEffect cleanup timing | Synchronous during unmount | Consistent async cleanup with transition support |
---
Debugging Tools Setup
React DevTools Extension
ALWAYS install the React DevTools browser extension for Chrome or Firefox. It adds the Components and Profiler tabs to browser DevTools.
- Chrome: Search "React Developer Tools" in Chrome Web Store
- Firefox: Search "React Developer Tools" in Firefox Add-ons
- Standalone (for React Native/iframe):
npx react-devtools
why-did-you-render Library
For granular re-render tracking beyond DevTools Profiler:
// wdyr.ts — MUST be imported BEFORE React
import React from "react";
if (process.env.NODE_ENV === "development") {
const whyDidYouRender = await import("@welldone-software/why-did-you-render");
whyDidYouRender.default(React, {
trackAllPureComponents: true,
});
}
// Then on specific components:
MyComponent.whyDidYouRender = true;---
Reference Links
- references/examples.md -- Debugging workflow examples with step-by-step instructions
- references/anti-patterns.md -- Common debugging mistakes and how to avoid them
Official Sources
- https://react.dev/learn/react-developer-tools
- https://react.dev/reference/react/StrictMode
- https://react.dev/reference/react/Profiler
- https://react.dev/errors
- https://react.dev/blog/2024/04/25/react-19
Debugging Anti-Patterns
Anti-Pattern 1: Removing StrictMode to Fix Double Renders
The Mistake
// WRONG: Removing StrictMode to stop "duplicate" effects
createRoot(document.getElementById("root")!).render(
// <StrictMode> <-- removed to "fix" double-render
<App />
// </StrictMode>
);Why It Is Wrong
StrictMode double-invokes components and effects on purpose to expose bugs:
- Impure render functions that produce side effects
- Effects without proper cleanup
- State initializers with side effects
Removing StrictMode does NOT fix the bug — it hides it. The bug will manifest in production as:
- Memory leaks from uncleared subscriptions
- Stale data from missing cleanup
- Race conditions from uncancelled async operations
The Correct Fix
ALWAYS keep StrictMode and fix the underlying issue:
// CORRECT: Keep StrictMode, fix the effect
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
// Fix: Add cleanup to your effect
useEffect(() => {
const controller = new AbortController();
fetchData({ signal: controller.signal }).then(setData);
return () => controller.abort(); // Cleanup cancels the fetch
}, []);---
Anti-Pattern 2: Using console.log Instead of DevTools
The Mistake
// WRONG: Littering components with console.log for debugging
function UserList({ users }: { users: User[] }): JSX.Element {
console.log("UserList rendered", users);
console.log("users length:", users.length);
return (
<ul>
{users.map((user) => {
console.log("rendering user:", user.id);
return <li key={user.id}>{user.name}</li>;
})}
</ul>
);
}Why It Is Wrong
- Console.log in render runs on EVERY render, flooding the console
- Does not show render timing or cause
- Must be manually added and removed
- Does not reveal the component tree or hook state
- StrictMode double-renders make output confusing
The Correct Approach
Use React DevTools Components tab to inspect props, state, and hooks. Use the Profiler tab to understand render timing and causes:
1. For state/props inspection: Select the component in DevTools Components tab 2. For render count/timing: Use the Profiler tab with "why did this render" enabled 3. For specific values: Use breakpoints in the Sources tab instead of console.log 4. For re-render tracking: Use why-did-you-render library
If you MUST use console.log, ALWAYS remove it before committing. NEVER ship console.log statements to production.
---
Anti-Pattern 3: Ignoring Key Warnings
The Mistake
// WRONG: Using array index as key for dynamic lists
function TodoList({ todos }: { todos: Todo[] }): JSX.Element {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}> {/* NEVER use index for reorderable lists */}
<input type="checkbox" checked={todo.done} />
{todo.text}
</li>
))}
</ul>
);
}Why It Is Wrong
When items are reordered, added, or removed from the middle of the list:
- React matches elements by key position, not by data identity
- Input state (checkbox, text input) stays attached to the wrong item
- DOM elements are unnecessarily destroyed and recreated
- Animations break because elements are treated as new
The Correct Fix
ALWAYS use a stable, unique identifier from the data:
// CORRECT: Stable unique key from data
function TodoList({ todos }: { todos: Todo[] }): JSX.Element {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}> {/* Stable ID from data */}
<input type="checkbox" checked={todo.done} />
{todo.text}
</li>
))}
</ul>
);
}Index keys are ONLY acceptable when ALL of these are true:
- The list is static (never reordered, filtered, or items added/removed)
- Items have no local state or uncontrolled inputs
- Items have no stable unique ID available
---
Anti-Pattern 4: Suppressing TypeScript Errors During Debugging
The Mistake
// WRONG: Suppressing errors to "make it work"
function UserProfile({ userId }: { userId: string }): JSX.Element {
const [user, setUser] = useState<User | null>(null);
// @ts-ignore — TODO: fix later
return <div>{user.name}</div>; // Runtime error: Cannot read properties of null
}Why It Is Wrong
TypeScript errors during debugging are SIGNALS, not obstacles:
userisnullinitially — the type system is telling you to handle the null case@ts-ignorehides the bug that will crash at runtime- "Fix later" comments become permanent technical debt
The Correct Approach
ALWAYS handle the types properly — they guide you to the fix:
// CORRECT: Handle the null state the type system warned about
function UserProfile({ userId }: { userId: string }): JSX.Element {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (user === null) {
return <div>Loading...</div>;
}
return <div>{user.name}</div>; // TypeScript knows user is User here
}---
Anti-Pattern 5: Debugging Production Without Source Maps
The Mistake
Trying to debug a production error like:
Uncaught Error: Minified React error #310; visit https://react.dev/errors/310
at a.js:1:4521
at b.js:1:891Without source maps, you cannot:
- See original file names or line numbers
- Set meaningful breakpoints
- Understand the actual code path
The Correct Approach
1. ALWAYS generate source maps for staging/QA environments:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // Generates .map files alongside bundles
},
});2. For production: Upload source maps to your error monitoring service (Sentry, Bugsnag) but do NOT serve them publicly:
// vite.config.ts — hidden source maps for production
export default defineConfig({
build: {
sourcemap: "hidden", // Generates .map files but does not reference them in bundles
},
});3. Look up minified error codes: Visit the URL in the error message (e.g., https://react.dev/errors/310) to see the full error text with parameter values.
---
Anti-Pattern 6: Debugging State by Mutating It
The Mistake
// WRONG: Mutating state to "test" a fix
function ItemList(): JSX.Element {
const [items, setItems] = useState<string[]>(["a", "b", "c"]);
const addItem = (): void => {
items.push("d"); // NEVER mutate state directly
setItems(items); // Same reference — React may skip the re-render
console.log(items); // Shows ["a", "b", "c", "d"] but UI may not update
};
return (
<div>
{items.map((item, i) => <span key={i}>{item}</span>)}
<button onClick={addItem}>Add</button>
</div>
);
}Why It Is Wrong
- React uses reference equality to detect state changes
- Mutating the existing array and calling
setStatewith the same reference may not trigger a re-render - Even when it does re-render, the "previous" state is corrupted because you mutated it
- This makes debugging harder because the state appears correct in console.log but the UI does not match
The Correct Approach
ALWAYS create new references when updating state:
// CORRECT: Immutable state update
const addItem = (): void => {
setItems((prev) => [...prev, "d"]); // New array reference
};---
Anti-Pattern 7: Wrapping Everything in ErrorBoundary Without Granularity
The Mistake
// WRONG: Single error boundary at the root catches everything
function App(): JSX.Element {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Header />
<Sidebar />
<MainContent />
<Footer />
</ErrorBoundary>
);
}
// If MainContent throws, the ENTIRE app shows the fallbackWhy It Is Wrong
- A single error boundary replaces the entire UI with the fallback
- Users lose access to navigation, sidebar, and all other functionality
- You cannot tell which section caused the error without reading the error log
- Recovery requires a full page reload
The Correct Approach
ALWAYS use granular error boundaries around independent UI sections:
// CORRECT: Granular error boundaries preserve working UI
function App(): JSX.Element {
return (
<>
<Header /> {/* Header stays visible even if content crashes */}
<div className="layout">
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
</div>
<Footer />
</>
);
}---
Anti-Pattern 8: Not Checking for Duplicate React Installations
The Mistake
Getting "Invalid hook call" errors and spending hours debugging hook logic, when the actual cause is two copies of React in the bundle.
Why It Happens
- A dependency bundles its own copy of React
npm linkor monorepo setup creates duplicate React instances- Mismatched
reactandreact-domversions
The Correct Diagnostic
ALWAYS check for duplicate React installations first when you see "Invalid hook call":
# Check for multiple React installations
npm ls react
npm ls react-dom
# Expected: single version at root level
# Problem: multiple versions at different depthsIf duplicates exist, resolve with:
# In the consuming package, force resolution to a single React
# package.json
{
"overrides": {
"react": "$react",
"react-dom": "$react-dom"
}
}ALWAYS verify react and react-dom are the exact same version. Mismatched versions cause hooks to fail silently.
Debugging Workflow Examples
Example 1: Diagnosing an Infinite Re-render Loop
Symptom
Console shows: "Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
Step-by-Step Debugging Workflow
1. Read the error stack trace — identify which component is looping:
Warning: Maximum update depth exceeded.
at SearchResults (SearchResults.tsx:12)
at Dashboard (Dashboard.tsx:5)
at App (App.tsx:3)2. Open the component and find all useEffect + useState pairs:
// BUG: This causes infinite re-renders
function SearchResults({ query }: { query: string }): JSX.Element {
const [results, setResults] = useState<Item[]>([]);
const [filters, setFilters] = useState({ sort: "name", order: "asc" });
// This creates a new object every render, so the effect re-runs every time
const options = { query, ...filters };
useEffect(() => {
fetchResults(options).then(setResults);
}, [options]); // options is a NEW object every render!
return <div>{results.map((r) => <ResultCard key={r.id} item={r} />)}</div>;
}3. Identify the problem: options is a new object reference every render, so the useEffect dependency is never stable.
4. Fix with useMemo or inline dependencies:
// FIXED: Stable dependencies
function SearchResults({ query }: { query: string }): JSX.Element {
const [results, setResults] = useState<Item[]>([]);
const [filters, setFilters] = useState({ sort: "name", order: "asc" });
useEffect(() => {
fetchResults({ query, ...filters }).then(setResults);
}, [query, filters.sort, filters.order]); // primitive deps are stable
return <div>{results.map((r) => <ResultCard key={r.id} item={r} />)}</div>;
}---
Example 2: Debugging a Hydration Mismatch
Symptom
Console shows: "Hydration failed because the initial UI does not match what was rendered on the server."
Step-by-Step Debugging Workflow
1. Check for browser-only values in render:
// BUG: window.innerWidth differs between server (undefined) and client
function Layout({ children }: { children: React.ReactNode }): JSX.Element {
const isMobile = window.innerWidth < 768; // NEVER access window during SSR render
return <div className={isMobile ? "mobile" : "desktop"}>{children}</div>;
}2. Fix with useEffect for client-only values:
// FIXED: Server and client render the same initial HTML
function Layout({ children }: { children: React.ReactNode }): JSX.Element {
const [isMobile, setIsMobile] = useState<boolean>(false);
useEffect(() => {
setIsMobile(window.innerWidth < 768);
const handleResize = (): void => setIsMobile(window.innerWidth < 768);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <div className={isMobile ? "mobile" : "desktop"}>{children}</div>;
}3. React 19 improvement: The error message now shows an exact diff of what mismatched:
Uncaught Error: Hydration failed because the initial UI does not match.
- Server: <div class="desktop">
+ Client: <div class="mobile">Common Hydration Mismatch Sources
| Source | Why It Mismatches | Fix |
|---|---|---|
Date.now() / new Date() | Different timestamp on server vs client | Pass timestamp as prop from server |
Math.random() | Different value each call | Use seeded random or generate server-side |
window / document access | Undefined on server | Guard with useEffect or typeof window |
| Browser extensions | Inject extra DOM nodes | React 19 handles <script>/<style> in head/body |
| Conditional rendering on auth | Server has no auth state | Use useEffect for auth-dependent UI |
---
Example 3: Performance Profiling a Slow List
Symptom
A list of 200 items feels sluggish when filtering. User types in a search box and there is visible lag.
Step-by-Step Profiling Workflow
1. Open React DevTools Profiler -> click gear icon -> enable "Record why each component rendered while profiling"
2. Record: Click the blue record button, type a character in the search box, click stop
3. Analyze the flamegraph:
- Look for the widest bars (longest render time)
- Grey bars did NOT render (good -- they were memoized or unchanged)
- Yellow/orange bars took significant time
4. Check "Why did this render?" for the slow component:
- "Props changed: items" — the parent is passing a new array reference
- "Hook 3 changed" — a hook value updated
5. Identify the problem:
// BUG: items is a new array every render, causing ALL ListItems to re-render
function SearchableList({ allItems }: { allItems: Item[] }): JSX.Element {
const [query, setQuery] = useState<string>("");
// This creates a new array every render
const filtered = allItems.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{filtered.map((item) => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
function ListItem({ item }: { item: Item }): JSX.Element {
// Expensive render (formatting, calculations)
return <div>{formatItem(item)}</div>;
}6. Fix with useMemo and React.memo:
import { memo, useMemo, useState } from "react";
function SearchableList({ allItems }: { allItems: Item[] }): JSX.Element {
const [query, setQuery] = useState<string>("");
const filtered = useMemo(
() =>
allItems.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase())
),
[allItems, query]
);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{filtered.map((item) => (
<MemoizedListItem key={item.id} item={item} />
))}
</div>
);
}
const MemoizedListItem = memo(function ListItem({ item }: { item: Item }): JSX.Element {
return <div>{formatItem(item)}</div>;
});7. Re-profile: Record the same interaction and compare. The flamegraph should show grey (skipped) bars for unchanged items.
---
Example 4: Debugging a Missing Cleanup in useEffect
Symptom
Strict Mode causes a subscription to fire twice, or data appears duplicated after navigating away and back.
Step-by-Step Debugging Workflow
1. Strict Mode reveals the issue by mounting, unmounting, and remounting the component. Console shows:
Connected to room: general
Connected to room: general // <-- duplicate! No cleanup happened2. Find the effect without cleanup:
// BUG: No cleanup — Strict Mode shows duplicate connections
function ChatRoom({ roomId }: { roomId: string }): JSX.Element {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
const socket = connectToRoom(roomId);
socket.on("message", (msg: Message) => {
setMessages((prev) => [...prev, msg]);
});
console.log(`Connected to room: ${roomId}`);
// MISSING: return cleanup function
}, [roomId]);
return <MessageList messages={messages} />;
}3. Add cleanup:
// FIXED: Cleanup disconnects on unmount and before re-running
function ChatRoom({ roomId }: { roomId: string }): JSX.Element {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
const socket = connectToRoom(roomId);
socket.on("message", (msg: Message) => {
setMessages((prev) => [...prev, msg]);
});
console.log(`Connected to room: ${roomId}`);
return () => {
socket.disconnect();
console.log(`Disconnected from room: ${roomId}`);
};
}, [roomId]);
return <MessageList messages={messages} />;
}4. Verify with Strict Mode: Console now shows the correct mount/unmount/remount pattern:
Connected to room: general
Disconnected from room: general // cleanup ran
Connected to room: general // remount — this is normal in StrictMode---
Example 5: Tracking Down "Cannot Update While Rendering"
Symptom
Console shows: "Cannot update a component (Parent) while rendering a different component (Child)."
Debugging Workflow
1. Identify the components from the warning — Parent state is being set inside Child render:
// BUG: Child sets Parent state during its own render
function Parent(): JSX.Element {
const [count, setCount] = useState<number>(0);
return <Child count={count} onUpdate={setCount} />;
}
function Child({ count, onUpdate }: { count: number; onUpdate: (n: number) => void }): JSX.Element {
if (count > 10) {
onUpdate(0); // NEVER call parent setState during render!
}
return <div>{count}</div>;
}2. Fix by moving to useEffect:
// FIXED: State update happens in an effect, not during render
function Child({ count, onUpdate }: { count: number; onUpdate: (n: number) => void }): JSX.Element {
useEffect(() => {
if (count > 10) {
onUpdate(0);
}
}, [count, onUpdate]);
return <div>{count}</div>;
}---
Example 6: Using why-did-you-render to Find Unnecessary Renders
Setup
// src/wdyr.ts — import this FIRST in your entry point
import React from "react";
if (process.env.NODE_ENV === "development") {
const whyDidYouRender = await import("@welldone-software/why-did-you-render");
whyDidYouRender.default(React, {
trackAllPureComponents: true,
logOnDifferentValues: true,
});
}// src/main.tsx
import "./wdyr"; // MUST be first import
import { createRoot } from "react-dom/client";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(<App />);Tag Components to Track
function ExpensiveTable({ data }: { data: Row[] }): JSX.Element {
return (
<table>
{data.map((row) => <TableRow key={row.id} row={row} />)}
</table>
);
}
// Enable tracking on this specific component
ExpensiveTable.whyDidYouRender = true;Interpreting Output
The console will show messages like:
ExpensiveTable
Re-rendered because of props changes:
data: {prev: Array(50), next: Array(50)}
Values are equal by value but not by reference.This tells you: the data array has the same contents but a new reference — memoize the array with useMemo in the parent.