ProgramGuru

Choosing State Structure

Choosing State Structure

An attendance page stores the students, the present count and the absent count separately. A teacher changes one student and forgets to update one count, leaving a contradictory report.

State structure is the shape of the information a component remembers. Keep the smallest complete set of changing facts, and calculate values that already follow from those facts while rendering.

  1. Understand

    Distinguish a stored fact from a calculated summary.

  2. Build

    Derive attendance counts from a student list.

  3. Check

    Avoid contradictions and duplicated student records.

Facts produce summaries

  1. Remember facts

    Keep each student and their present flag.

  2. Calculate

    Count present entries during rendering.

  3. Describe

    Show counts consistent with the same list.

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

Store facts, derive consequences

A present flag is an independent fact: the teacher can change it. The present count follows from the flags, and the absent count follows from the class size minus the present count. Storing all three creates more obligations whenever attendance changes. Deriving the counts means there is only one attendance record to update.

Derived means calculated from information you already have. It does not mean a special React API. Ordinary filter and subtraction are enough for this small class. If a selected student is needed, store a stable student ID and find the matching entry instead of storing another copy of the entire student object.

Calculate a consistent attendance summary

The class contains Asha and Ravi. Only the list belongs in state. Each render calculates present and absent from the list. Mark Ravi present replaces the matching object with a new object, preserving Asha’s record. The labels then follow automatically from the updated list.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([
    { id: 1, name: 'Asha', present: true },
    { id: 2, name: 'Ravi', present: false }
  ]);
  const present = students.filter(student => student.present).length;
  const absent = students.length - present;
  return <main><h1>Attendance facts</h1>
    <p>Present: {present}; Absent: {absent}</p>
    <ul>{students.map(student => <li key={student.id}>
      {student.name}: {student.present ? 'present' : 'absent'}
    </li>)}</ul>
    <button onClick={() => setStudents(current => current.map(student =>
      student.id === 2 ? { ...student, present: true } : student
    ))}>Mark Ravi present</button>
  </main>;
}

Expected screen and behavior

Initially Present: 1 and Absent: 1. Mark Ravi present changes the counts to 2 and 0.

One update changes several descriptions

React · Calculate a consistent attendance summary

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([
    { id: 1, name: 'Asha', present: true },
    { id: 2, name: 'Ravi', present: false }
  ]);
  const present = students.filter(student => student.present).length;
  const absent = students.length - present;
  return <main><h1>Attendance facts</h1>
    <p>Present: {present}; Absent: {absent}</p>
    <ul>{students.map(student => <li key={student.id}>
      {student.name}: {student.present ? 'present' : 'absent'}
    </li>)}</ul>
    <button onClick={() => setStudents(current => current.map(student =>
      student.id === 2 ? { ...student, present: true } : student
    ))}>Mark Ravi present</button>
  </main>;
}

On the first render, filter includes only Asha, so present is 1. There are two students, so absent is also 1. Clicking the button gives the updater the current list. map produces the next list, replacing Ravi’s present flag without mutating the old object.

React renders the component with that next list. filter now includes both entries and the same expression returns 2. The subtraction returns 0. There is no timing gap between a list update and a separate count update because the counts are computed from the very list used to describe this screen.

Choose shapes that prevent impossible cases

When a form has idle, sending and sent stages, one status string is often clearer than independent isSending and isSent booleans. Separate booleans can accidentally allow both to be true. A single status describes one stage at a time, and the UI derives button labels from that stage.

Flatten deeply nested information when frequent changes require many levels of copying. Group fields that must change together, but keep unrelated values independent when that improves clarity. There is no rule that every page needs one huge state object; the goal is making valid updates easy to express and invalid combinations harder to create.

Try the school task

React · starting example

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([
    { id: 1, name: 'Asha', present: true },
    { id: 2, name: 'Ravi', present: false }
  ]);
  const present = students.filter(student => student.present).length;
  const absent = students.length - present;
  return <main><h1>Attendance facts</h1>
    <p>Present: {present}; Absent: {absent}</p>
    <ul>{students.map(student => <li key={student.id}>
      {student.name}: {student.present ? 'present' : 'absent'}
    </li>)}</ul>
    <button onClick={() => setStudents(current => current.map(student =>
      student.id === 2 ? { ...student, present: true } : student
    ))}>Mark Ravi present</button>
  </main>;
}

Show a percentage of present students without adding percentage state. The classroom can be empty, so choose an explicit display rule for zero students. For the two students here, one present should display 50% and both present should display 100%. Keep the percentage consistent with the count.

Worked answer

The answer derives a percentage and guards division by zero. Math.round is the chosen display policy; other rounding policies are possible. Notice that the answer still has only one changing source, students. The button changes a fact, while the percentage is calculated again on the next render.

React · JavaScript with JSX

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([
    { id: 1, present: true }, { id: 2, present: false }
  ]);
  const present = students.filter(student => student.present).length;
  const percent = students.length === 0 ? 0 : Math.round(present / students.length * 100);
  return <main><h1>Attendance percentage</h1><p>Present: {percent}%</p>
    <button onClick={() => setStudents(current => current.map(student =>
      ({ ...student, present: true })
    ))}>Mark all present</button>
  </main>;
}

Expected screen and behavior

The initial percentage is 50%. Mark all present changes it to 100%. An empty list follows the chosen 0% policy.

Mistakes and boundaries

Adding an Effect to maintain the count creates an unnecessary extra update and another place to make mistakes. Calculate inexpensive display values during rendering. An expensive calculation may justify measurement and an optimization later, but it still does not become an independent school fact.

A selected object copied into state can become stale after its original list entry changes. Keep selectedId and look up the current record. Use stable IDs rather than positions when list ordering can change, and handle a missing ID explicitly after a student is removed.

Check your reasoning

React · Calculate a consistent attendance summary

import { useState } from 'react';
export default function App() {
  const [students, setStudents] = useState([
    { id: 1, name: 'Asha', present: true },
    { id: 2, name: 'Ravi', present: false }
  ]);
  const present = students.filter(student => student.present).length;
  const absent = students.length - present;
  return <main><h1>Attendance facts</h1>
    <p>Present: {present}; Absent: {absent}</p>
    <ul>{students.map(student => <li key={student.id}>
      {student.name}: {student.present ? 'present' : 'absent'}
    </li>)}</ul>
    <button onClick={() => setStudents(current => current.map(student =>
      student.id === 2 ? { ...student, present: true } : student
    ))}>Mark Ravi present</button>
  </main>;
}

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

Which information needs state in this attendance example?
Choose one answer. You can try again.

Answer and explanation

The student present flags; present and absent counts can be calculated.

Correct. The flags are changing facts and the counts follow from them.

Carry the idea forward

Prefer state that expresses the underlying school facts once. Calculate summaries from those facts and represent exclusive stages with a single status. Continue with preserving and resetting state to see how React associates remembered values with component identity.

Read the primary reference: React: choosing the state structure.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Preserving and Resetting State →