ProgramGuru

Debugging React: Follow Props, State and Ownership

Debugging React: Follow Props, State and Ownership

An attendance counter changes internally, but the displayed list disagrees. Randomly adding Effects can make the problem harder to see; a reproducible interaction and a clear ownership trace reveal the first mismatch.

Debugging is finding why actual behavior differs from an explicit expected result. In React, inspect the data owner, rendered props and state, event updates, keys and synchronization before changing code.

  1. Understand

    Reproduce a minimal failing interaction.

  2. Build

    Trace an immutable state update to its rendered result.

  3. Check

    Use development tools without treating logs as a timing guarantee.

Find the first divergence in the data flow

  1. Expected

    One attendance toggle changes one student.

  2. Trace

    Inspect the owner and the next state produced.

  3. Verify

    Repeat the interaction and check derived counts.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

Make the failure specific before fixing it

Write down the input and interaction: Maya begins absent, one click should mark her present, and the present count should become one. This turns “React is broken” into a checkable observation. Inspect whether the handler ran, whether it produced the next state and whether the rendered expression reads the correct value.

React Developer Tools can inspect component props and state. Browser tools help inspect DOM, network responses and console errors. Logging in a render shows an attempted render and may appear more than once during development checks; do not infer that every log means a separate user-visible commit.

Expose a clear state trace for attendance

The button uses map to create a new array and object rather than mutate the existing record. The summary is derived from that same array. This corrected, complete example is the reference behavior to compare against a failing implementation.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }]);
  function toggle() {
    setStudents(current => current.map(student => student.id === 1 ? { ...student, present: !student.present } : student));
  }
  const count = students.filter(student => student.present).length;
  return <main><h1>Attendance trace</h1><p>Present count: {count}</p>
    <p>Maya: {students[0].present ? 'present' : 'absent'}</p><button onClick={toggle}>Toggle Maya</button></main>;
}

Expected screen and behavior

Toggle Maya changes the flag and derived Present count: 1 together.

Find mutation and stale reads before adding synchronization

React · Expose a clear state trace for attendance

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }]);
  function toggle() {
    setStudents(current => current.map(student => student.id === 1 ? { ...student, present: !student.present } : student));
  }
  const count = students.filter(student => student.present).length;
  return <main><h1>Attendance trace</h1><p>Present count: {count}</p>
    <p>Maya: {students[0].present ? 'present' : 'absent'}</p><button onClick={toggle}>Toggle Maya</button></main>;
}

A broken version might assign students[0].present directly and call setStudents(students). That reuses the same array identity and mutates earlier state. React may skip the update because the provided state is equal to the old state by Object.is. The corrected source replaces both the array and the changed record.

Another failure can read a state snapshot immediately after setting it and assume it already changed. Inspect the next render or use the functional updater’s input when calculating from pending state. An Effect is not a general patch for ownership, mutation or snapshot misunderstandings.

Match debugging tools to the suspected layer

If the handler is not called, inspect its prop, disabled state and event target. If the wrong row changes, inspect the stable student ID and list key rather than just the row index. If network data is wrong, inspect status and payload shape before blaming rendering.

Reduce a complex page to the smallest example that still fails. Add a focused regression test after identifying the cause, so later changes cannot quietly restore the same bug. Keep unrelated refactoring out of the fix until the observed failure is explained.

Try the school task

React · starting example

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }]);
  function toggle() {
    setStudents(current => current.map(student => student.id === 1 ? { ...student, present: !student.present } : student));
  }
  const count = students.filter(student => student.present).length;
  return <main><h1>Attendance trace</h1><p>Present count: {count}</p>
    <p>Maya: {students[0].present ? 'present' : 'absent'}</p><button onClick={toggle}>Toggle Maya</button></main>;
}

Add Ravi and toggle only his attendance. Verify that Maya remains absent and the derived count changes to one.

Worked answer

The condition selects Ravi by ID rather than array position. The unchanged Maya object can be retained, while Ravi gets a replacement. The same list supplies both the labels and count.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }, { id: 2, name: 'Ravi', present: false }]);
  const count = students.filter(student => student.present).length;
  return <main><h1>Targeted attendance</h1><p>Present count: {count}</p>
    <ul>{students.map(s => <li key={s.id}>{s.name}: {s.present ? 'present' : 'absent'}</li>)}</ul>
    <button onClick={() => setStudents(current => current.map(s => s.id === 2 ? { ...s, present: !s.present } : s))}>Toggle Ravi</button></main>;
}

Expected screen and behavior

Toggle Ravi makes Ravi present and count one while Maya remains absent.

Mistakes and boundaries

Development StrictMode can repeat render work to reveal impurities. Do not “fix” repeated logs by mutating global counters or disabling checks before understanding the behavior.

Do not paste private student data, credentials or entire production responses into public debugging reports. Use a small synthetic reproduction with the same relevant shape.

A passing local example does not verify a deployed network, router fallback or service permission. Test the layer where the failure actually occurred.

Check your reasoning

React · Expose a clear state trace for attendance

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([{ id: 1, name: 'Maya', present: false }]);
  function toggle() {
    setStudents(current => current.map(student => student.id === 1 ? { ...student, present: !student.present } : student));
  }
  const count = students.filter(student => student.present).length;
  return <main><h1>Attendance trace</h1><p>Present count: {count}</p>
    <p>Maya: {students[0].present ? 'present' : 'absent'}</p><button onClick={toggle}>Toggle Maya</button></main>;
}

Choose the answer that follows the data and the interaction, then read the explanation for each alternative.

Why is mutating the old array and passing that same array to the setter unreliable?
Choose one answer. You can try again.

Answer and explanation

It mutates earlier state and can supply the same identity React uses to skip an unchanged update.

Correct. Produce a deliberate next value instead.

Carry the idea forward

Reproduce, trace the first mismatch, fix its cause and verify the original task. Review array updates and regression checks.

Read the primary reference: React: developer tools.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Building and Deploying: Serve the Right Runtime →