Learning path · Level 9 · Tutorial 4

Update Component State from User Events

Make components respond to user events with typed local state, predictable queued updates, and immutable changes, using lint diagnostics to catch invalid Hook placement.

  • Level: Intermediate
  • Duration: 22 min
  • 9 steps
Start tutorial Back to the path
Update Component State from User Events

What you will go through

  1. Connect a Button to Local State Wire a button callback to inferred numeric state and explain which component instance changes. 3 min
  2. Declare the State Values You Allow Select inferred or explicit state types that support intended updates without allowing invalid values. 2 min
  3. Fix Invalid Hook Placement Correct an ESLint-reported Hook placement issue by keeping useState at the component top level and verifying the repair locally. 2 min
  4. Read State as a Render Snapshot Understand why a state setter requests a future render instead of changing the value already captured by a running event handler. 2 min
  5. Apply Queued Updates Predictably Choose functional updaters for cumulative changes, process queued replacements and updaters in order, and predict the rendered count after one event handler. 3 min
  6. Replace Object State Without Losing Fields Update object state immutably by returning a complete replacement that preserves unaffected fields. 2 min
  7. Add, Change, and Remove Array Records Update a typed drill collection by appending, replacing, and removing records without mutation. 2 min
  8. Keep Rendering and Updaters Pure Separate event-handler side effects from pure state calculations and rendering, including development Strict Mode checks. 2 min
  9. Repair and Verify the Practice Tracker Complete, repair, and locally verify the PracticeTracker using typed state, functional updaters, immutable replacements, and lint-guided Hook placement. 4 min

What you will learn

  • Connect event-handler callbacks to typed component state.
  • Predict the result of snapshot-based and queued state updates.
  • Use functional updaters and immutable replacements for state changes.
  • Correct an invalid Hook call reported by ESLint without disabling the rule.
  • Keep rendering and updater functions free of side effects.

Before you start

  • Render Conditional Content and Keyed Lists
  • Make Non-Mutating Updates with Spread Syntax
  • Handle Browser Events with Listeners and Delegation

Step 1 of 9

Connect a Button to Local State

Wire a button callback to inferred numeric state and explain which component instance changes.

State is local component memory

Local memory belongs to an instance

useState gives each mounted component instance its own local memory. The returned pair contains the current value and a setter that requests a changed value for a later render.

In PracticeTracker, useState(0) lets TypeScript infer that repetitions is a number and that setRepetitions accepts number values. If a page renders two <PracticeTracker /> instances, they do not share a count: each instance owns its own repetitions value.

One event updates one component instance

Diagram of two separate tracker instances where clicking the left button changes only the left count from zero to one.

The clicked button's event handler requests an update for the component instance that owns that button.

Connect a click to the setter

Connect buttons to local state

Replace the contents of src/App.tsx with this example. Keep the existing StrictMode setup in src/main.tsx. A named event handler and an inline arrow callback can both request state updates from native buttons.

tsx
import { useState } from 'react';

function PracticeTracker() {
  const [repetitions, setRepetitions] = useState(0);

  function handleAddOne() {
    setRepetitions(repetitions + 1);
  }

  return (
    <section>
      <p>Repetitions: {repetitions}</p>
      <button type="button" onClick={handleAddOne}>
        Add 1
      </button>
      <button
        type="button"
        onClick={() => setRepetitions(repetitions + 3)}
      >
        Add 3
      </button>
    </section>
  );
}

export default function App() {
  return <PracticeTracker />;
}
Expected output
Initial interface:
Repetitions: 0
[Add 1] [Add 3]

After clicking Add 1 from the initial interface:
Repetitions: 1

After clicking Add 3 from the initial interface:
Repetitions: 3

After clicking Add 1 and then Add 3:
Repetitions: 4

The count shown after a click depends on the count from which that click starts. The examples below show each action from the initial count, plus the result of clicking them in sequence.

Tip

Pass the callback; do not call it

Use onClick={handleAddOne}, not onClick={handleAddOne()}. The first passes an event handler callback for React to call later when a click occurs. The second invokes the function immediately while React is rendering, so it does not register the intended click callback.

Register the handler for later

Choose the event-handler form

Which button correctly registers handleAddOne to run when the user clicks it?

Apply the pattern

Apply the local-state connection

Write the missing state declaration and the complete handleAddOne function for this component. Then write the button's onClick expression and state which tracker changes if an app renders two <PracticeTracker /> instances and the user presses Add 1 in the second one.

function PracticeTracker() {
  // Write the inferred numeric state pair here.

  // Write handleAddOne here.

  return (
    <button type="button" onClick={/* write the expression */}>
      Add 1
    </button>
  );
}

Write at least 80 characters (0/80).

Step 2 of 9

Declare the State Values You Allow

Select inferred or explicit state types that support intended updates without allowing invalid values.

Infer what the initial value can describe

When inference needs help

A primitive initial value can describe its own state type: useState(0) gives repetitions the type number. When an initial value intentionally does not show every future possibility, provide a type argument to useState.

For example, null does not reveal that a message may later be a string, and [] does not reveal which record type belongs in the array. Describe all permitted values instead of using any or an unsafe assertion.

Initial values and future values

A comparison of numeric state, nullable message state, and a typed empty list of drill records.

An initial value can be narrower than the complete set of values that state must support.

Choose a type that includes future values

Declare every allowed state value

Use a literal union to limit state to supported strings. Include null when a value is absent at first but may later be a string. For an initially empty array, provide the array element type.

The setter accepts only values compatible with the declared state type. These isolated typing examples illustrate allowed values; they are not required features of the final tracker.

State type choices

number is not an integer-only TypeScript type. The tracker’s event handlers maintain whole-number counts by the values they calculate, but TypeScript's number type itself also permits fractional values, NaN, and infinities.

Explicit type arguments express values that an initial value alone cannot describe.
StateInitial valueDeclarationPermitted type
repetitions0useState(0)number
mode'practice'useState<Mode>('practice')'practice' or 'review'
lastMessagenulluseState<string | null>(null)string or null
drills[]useState<Drill[]>([])array of Drill records

Typed state declarations

These declarations belong inside a component. Reusable type aliases can remain outside it.

tsx
import { useState } from 'react';

type Mode = 'practice' | 'review';
type Drill = { id: string; label: string; done: boolean };

function PracticeTracker() {
  const [mode, setMode] = useState<Mode>('practice');
  const [lastMessage, setLastMessage] = useState<string | null>(null);
  const [drills, setDrills] = useState<Drill[]>([]);

  return null;
}

Check compatible updates

Match updates to compatible types

Match each proposed setter call to the state type that accepts its replacement value.

Tap an item and then its matching pair.

Apply the allowed state types

Complete declarations and match updates

Complete the union, nullable, and empty-array declarations. Then complete the state type matched by each proposed update.

tsx
import { useState } from 'react';

type Drill = { id: string; label: string; done: boolean };
type Mode = ;

function PracticeTracker() {
  const [mode, setMode] = useState<Mode>('practice');
  const [lastMessage, setLastMessage] = useState<>(null);
  const [drills, setDrills] = useState<>([]);

  // Match each proposed update to its accepting state type:
  // setMode('review') -> 
  // setLastMessage('Saved') -> 
  // setDrills([]) -> 

  return null;
}

Step 3 of 9

Fix Invalid Hook Placement

Correct an ESLint-reported Hook placement issue by keeping useState at the component top level and verifying the repair locally.

Why Hook placement matters

Keep Hook calls in a stable order

A Hook must run in the same order every time its component renders. Call useState at the top level of PracticeTracker, before any conditional return.

If an early return comes first, one render can reach the Hook while another returns before it. React then cannot reliably associate that Hook call with its stored state.

Compare the render paths

A comparison of an unsafe component path that can return before reaching state and a safe path that calls state before branching.

The safe outline calls the Hook first, then decides which interface output to return.

Warning

Invalid Hook locations

Do not call Hooks inside conditions, loops, nested callbacks, or event handlers. An event handler can call a setter that was created by a top-level Hook call, but it must not declare another Hook.

Read the actual diagnostic

Create a reproducible lint failure

Temporarily replace the contents of src/App.tsx with this complete broken example. It still renders because shouldRenderTracker is currently true, but the Hook is below an early return. Save the file, then run the lint command before making a repair.

A Hook after an early return

Broken src/App.tsx

tsx
import { useState } from 'react';

const shouldRenderTracker = true;

function PracticeTracker() {
  if (!shouldRenderTracker) {
    return null;
  }

  const [repetitions, setRepetitions] = useState(0);

  function handleAddOne() {
    setRepetitions(repetitions + 1);
  }

  return (
    <section>
      <p>Repetitions: {repetitions}</p>
      <button type="button" onClick={handleAddOne}>
        Add 1
      </button>
    </section>
  );
}

export default function App() {
  return <PracticeTracker />;
}

Inspect the ESLint diagnostic

Run lint before changing the file. Read the source location, the message, and the exact rule name.

bash
npm run lint
Expected output
> eslint .

src/App.tsx
  10:41  error  React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?  react-hooks/rules-of-hooks

✖ 1 problem (1 error, 0 warnings)

The npm script heading can vary by project. For the code shown, the important details are location `10:41`, the conditional Hook message, and `react-hooks/rules-of-hooks`.

Repair and verify locally

Move the Hook above the return

Complete the missing top-level declaration before the conditional return.

tsx
function PracticeTracker() {
  

  if (!shouldRenderTracker) {
    return null;
  }

  function handleAddOne() {
    setRepetitions(repetitions + 1);
  }

  return <button type="button" onClick={handleAddOne}>Add 1</button>;
}

Run lint again

Make the same move in your saved src/App.tsx: cut the existing state declaration and paste it immediately inside PracticeTracker, before the if statement. Keep the conditional output and keep react-hooks/rules-of-hooks enabled. Save, then verify the repair.

bash
npm run lint
Expected output
> eslint .

The npm heading can differ by project. A successful lint run finishes with no ESLint diagnostic after the script command.

Locate, repair, and verify

Locate, repair, and verify

Use this short component to demonstrate the complete lint–repair–lint workflow. Identify the invalid Hook call, write the corrected opening of the component with the declaration in its valid position, and list the three local actions in order: initial lint, source repair, and verification lint.

function PracticeTracker() {
  if (!shouldRenderTracker) {
    return null;
  }

  const [repetitions, setRepetitions] = useState(0);

  return <p>Repetitions: {repetitions}</p>;
}

Write at least 180 characters (0/180).

Step 4 of 9

Read State as a Render Snapshot

Understand why a state setter requests a future render instead of changing the value already captured by a running event handler.

State belongs to a particular render

One render, one snapshot

A state value is a state snapshot: the value available to one particular render of a component. The event handlers created during that render read that same snapshot.

When an event handler calls a setter, React receives a request for a new value. It does not rewrite the repetitions variable that the currently running handler captured. After the handler finishes, React can render again with the requested value.

From click to the next render

A timeline shows a click handled with a count of zero, followed by a new rendered interface with a count of one.

The handler reads the current render's snapshot. The requested value appears in the interface produced by the next render.

Tip

Key distinction

Think of the setter as sending a request to React, not as assigning to a local variable. A later render creates new handlers that close over the new state snapshot.

A setter does not change the current handler

Trace a handler

Trace this first click when the displayed count starts at 0. Both console messages run inside the same event handler, so both read the snapshot captured by that handler. Only the interface from the subsequent render displays the requested count.

PracticeTracker snapshot example

Study this component and its first click.

tsx
import { useState } from 'react';

export function PracticeTracker() {
  const [repetitions, setRepetitions] = useState(0);

  function handleAddOne() {
    console.log('Before request:', repetitions);
    setRepetitions(repetitions + 1);
    console.log('After request:', repetitions);
  }

  return (
    <section>
      <p>Repetitions: {repetitions}</p>
      <button type="button" onClick={handleAddOne}>
        Add 1
      </button>
    </section>
  );
}

Tip

Why both reads match

The expression repetitions + 1 is calculated from the current snapshot. Calling setRepetitions requests that calculated value for a later render; it does not alter either console read in the handler already running.

Identify the captured value

Check the idea

Complete the sentence: Each event handler reads the state __ from the render that created it.

Apply the snapshot model

Predict the current and next values

The interface currently displays Repetitions: 4. The user clicks Add 1, which runs this handler:

function handleAddOne() {
  const before = repetitions;
  setRepetitions(repetitions + 1);
  const after = repetitions;
}

State the values of before, after, and the count displayed after React renders again. Then explain why the two variables inside the handler have their values.

Write at least 70 characters (0/70).

Step 5 of 9

Apply Queued Updates Predictably

Choose functional updaters for cumulative changes, process queued replacements and updaters in order, and predict the rendered count after one event handler.

Queue updates instead of expecting an immediate render

One handler uses one snapshot

React queues state updates requested during one event handler. It normally waits until that handler finishes before rendering the interface again.

A handler reads one state snapshot. If repetitions is 0, each repetitions + 1 expression below is calculated from that same snapshot. Each call therefore requests the same replacement value, rather than building on the prior call.

Three snapshot-based requests

Three calls, but each expression reads the same repetitions value from this render.

tsx
function handleAddThree() {
  setRepetitions(repetitions + 1);
  setRepetitions(repetitions + 1);
  setRepetitions(repetitions + 1);
}

Two ways a queued update can behave

A comparison diagram: three updates calculated from one snapshot all request 1, while three pending-state updates progress from 0 to 3.

Snapshot-based expressions can request the same replacement value. Functional updaters receive the pending value created by earlier queue entries.

Build from the pending value

Choose the update form from the intent

Use a functional updater when the next value is calculated from the previous or pending value. React processes each updater in call order, passing it the value produced by the preceding queued entry.

A direct replacement value is still useful when you deliberately want a fixed value. In a mixed queue, process both kinds of entry in their written order.

A cumulative Add 3 handler

Each functional updater receives the pending value, not the original handler snapshot.

tsx
function handleAddThree() {
  setRepetitions(previous => previous + 1);
  setRepetitions(previous => previous + 1);
  setRepetitions(previous => previous + 1);
}

Process a mixed queue in order

Starting from repetitions = 0, this queue mixes deliberate replacement values with functional updaters.

The later entry always receives the pending result of the earlier entry.
Queued entryPending value afterward
setRepetitions(10)10
previous => previous - 28
setRepetitions(4)4
previous => previous + 15

Practice: repair the queued increment

Repair Add 3

Complete handleAddThree using three functional updater calls. Each call must add one.

tsx
function handleAddThree() {
  
}

Apply snapshot, updater, and replacement rules

Final queue challenge

Starting from repetitions = 0, evaluate these three separate handlers:

A calls setRepetitions(repetitions + 1) three times.

B calls setRepetitions(previous => previous + 1) three times.

C queues these entries in order:

setRepetitions(5);
setRepetitions(previous => previous + 1);
setRepetitions(2);
setRepetitions(previous => previous * 3);

Report the final count for A, B, and C. Then write a handleAddThree function that queues three functional increments.

Write at least 80 characters (0/80).

Step 6 of 9

Replace Object State Without Losing Fields

Update object state immutably by returning a complete replacement that preserves unaffected fields.

Replace the whole preferences object

Object state is replaced

An object in state is a single value. Treat preferences as read-only during rendering and in event handlers. When you call setPreferences, React stores the replacement object you provide; it does not merge missing fields from the previous object.

To change showTips while retaining dailyGoal, create a new object. Because the next value is calculated from the current object, use a functional updater. It receives the pending previous state and returns the replacement.

Mutation versus replacement

A comparison between mutating one preferences object and creating a separate replacement object that preserves the daily goal.

A safe update keeps the original object unchanged and returns a distinct replacement object.

Toggle one property without losing another

This handler creates a replacement object: spread copies the existing fields, then the later showTips property overrides that one field.

tsx
import { useState } from 'react';

function PracticeTracker() {
  const [preferences, setPreferences] = useState({
    dailyGoal: 5,
    showTips: true
  });

  function handleToggleTips() {
    setPreferences(previous => ({
      ...previous,
      showTips: !previous.showTips
    }));
  }

  return (
    <section>
      <p>Daily goal: {preferences.dailyGoal}</p>
      <p>Tips: {preferences.showTips ? 'shown' : 'hidden'}</p>
      <button type="button" onClick={handleToggleTips}>
        Toggle tips
      </button>
    </section>
  );
}

Preserve fields with a functional updater

Why a new object matters

Avoid assigning into the state object, such as changing a property on preferences and then passing that same object to the setter. The old and new references would be identical, so React can skip the update. More importantly, mutation makes the previous state unreliable.

A functional updater should only calculate and return the next object. It must not modify previous before returning it.

Tip

A small ordering rule

Put the spread first and the changed property after it. Property order matters here: the later property value replaces the copied value for that key. The functional updater also remains correct if React processes other pending updates before this one.

Recognize two unsafe assumptions

Check replacement behavior

True or false: calling setPreferences({ showTips: false }) automatically retains dailyGoal from the existing preferences object.

Check mutation behavior

True or false: a functional updater may change previous.showTips and return previous because it was called through a setter.

Apply the replacement pattern

Complete the immutable preferences update

Complete the updater so that clicking Toggle tips changes the tips setting while retaining the daily goal.

tsx
function handleToggleTips() {
  setPreferences(previous => ({
    ,
    showTips: 
  }));
}

Step 7 of 9

Add, Change, and Remove Array Records

Update a typed drill collection by appending, replacing, and removing records without mutation.

Keep record updates immutable

Replace the collection and the changed record

drills is an array of record references. Treat both the array and every record inside it as read-only state.

For each change, return replacements that describe the next collection:

  • Add: return a new array containing a fixed drill that is not already present.
  • Complete: return a new array and replace only the matching drill record.
  • Remove: return a new array without the matching drill.

Copying only the outer array is not enough if you then change a property on an existing record. The copied array would still contain a reference to a record from the previous state.

Why an outer-array copy is not always enough

A reference diagram contrasting a safe replacement of a changed drill record with copying only an array and changing an original nested record.

Completing a drill requires a new array and a replacement for the matched record. Unchanged records can be reused.

Use append, map, and filter

Append a fixed drill without duplicates

Keep fixed records at module scope. The Hook and the event handler must be inside PracticeTracker, where the Hook has a valid top-level position. Each click adds the first fixed drill whose ID is absent; when both records are present, it returns the existing collection unchanged.

tsx
import { useState } from 'react';

type Drill = { id: string; label: string; done: boolean };

const fixedDrills: Drill[] = [
  { id: 'drill-1', label: 'Trace a state update', done: false },
  { id: 'drill-2', label: 'Check Hook placement', done: false }
];

function PracticeTracker() {
  const [drills, setDrills] = useState<Drill[]>([]);

  function handleAddDrill() {
    setDrills(previous => {
      const nextDrill = fixedDrills.find(
        candidate => !previous.some(drill => drill.id === candidate.id)
      );
      return nextDrill ? [...previous, nextDrill] : previous;
    });
  }

  return (
    <button type="button" onClick={handleAddDrill}>
      Add drill
    </button>
  );
}

Complete or remove a targeted drill

Add these handlers inside the same PracticeTracker component, after its drills state declaration. map retains every position while replacing the matched record. filter retains every record except the matched one.

tsx
function handleComplete(id: string) {
  setDrills(previous =>
    previous.map(drill => (
      drill.id === id ? { ...drill, done: true } : drill
    ))
  );
}

function handleRemove(id: string) {
  setDrills(previous =>
    previous.filter(drill => drill.id !== id)
  );
}

// A rendered record supplies the target ID:
// onClick={() => handleComplete(drill.id)}

Tip

Target records by ID

Use stable IDs to target a record, not its current array position. The Complete and Remove callbacks need an ID because each action affects one displayed drill; they do not need the browser event object.

Practice a safe nested update

Practice: replace the matching record

Complete the immutable replacement for the matching drill.

tsx
setDrills(previous =>
  previous.map(drill =>
    drill.id === 'drill-1'
      ? 
      : drill
  )
);

Apply the collection rules

Apply all three collection update patterns

Write three functional updater bodies for setDrills:

  1. Append the first record in fixedDrills whose ID is not already in previous; if no such record exists, keep previous.
  2. Mark the record with ID 'drill-1' as complete without mutating it.
  3. Remove the record with ID 'drill-2'.

Then inspect this proposed completion update. Identify the nested mutation and explain why copying its outer array does not make it immutable:

const next = [...previous];
next[0].done = true;
return next;

Write at least 180 characters (0/180).

Step 8 of 9

Keep Rendering and Updaters Pure

Separate event-handler side effects from pure state calculations and rendering, including development Strict Mode checks.

Separate the work by responsibility

Three responsibilities

A render calculates JSX from the current props and state snapshot. It should not change external data, write storage, make a request, or perform an action intended for a user click.

An event handler is the place for work caused by that click. A functional updater has one small job: calculate and return the next state from previous without mutation or other side effects.

From click to updated interface

A three-part diagram separates a button click and console message in an event handler, a pure state calculation, and the next rendered repetition count.

Keep user-triggered actions in the event handler; keep state calculations and rendering pure.

Write code that tolerates development checks

Keep the updater focused

A console message is an action-specific side effect, so put it in the event handler. The functional updater only returns the next number. Rendering only describes the interface.

This structure stays reliable if React checks the component and updater more than once during development.

A pure Add 1 action

A pure updater returns a value; the handler owns the console message.

tsx
import { useState } from 'react';

export function PracticeTracker() {
  const [repetitions, setRepetitions] = useState(0);

  const handleAddOne = () => {
    console.log('Add 1 clicked');
    setRepetitions(previous => previous + 1);
  };

  return (
    <section>
      <p>Repetitions: {repetitions}</p>
      <button type="button" onClick={handleAddOne}>
        Add 1
      </button>
    </section>
  );
}

Warning

Development check, not duplicate click

In development, Strict Mode may repeat rendering and functional-updater calls to expose impure logic. Do not rely on an exact number of render or updater logs. Strict Mode does not deliberately invoke an event handler twice, so one click is still one user interaction.

Practice a pure updater

Complete the state calculation

Complete the pure functional updater so the Add 1 action increases the current repetition count by one.

tsx
const handleAddOne = () => {
  console.log('Add 1 clicked');
  setRepetitions(previous => );
};

Apply the purity rules

Check responsibility boundaries

Match each situation to the correct conclusion.

Tap an item and then its matching pair.

Step 9 of 9

Repair and Verify the Practice Tracker

Complete, repair, and locally verify the PracticeTracker using typed state, functional updaters, immutable replacements, and lint-guided Hook placement.

Repair the starter locally

Start from the broken tracker

Final local repair

In src/App.tsx, repair the complete starter below. It deliberately contains one invalid Hook placement and several state changes that are not immutable.

Keep useState calls at the component's top level, before the early return. Do not disable react-hooks/rules-of-hooks. Then replace the update logic so it uses predictable queued updates and immutable replacements.

src/App.tsx

Starter code — part 1 of 2

tsx
import { useState } from 'react';

type Drill = {
  id: string;
  label: string;
  done: boolean;
};

const fixedDrills: Drill[] = [
  { id: 'drill-1', label: 'Trace a state update', done: false },
  { id: 'drill-2', label: 'Check Hook placement', done: false }
];

const shouldRenderTracker = true;

function PracticeTracker() {
  if (!shouldRenderTracker) {
    return null;
  }

  // ESLint reports react-hooks/rules-of-hooks here.
  const [repetitions, setRepetitions] = useState(0);
  const [preferences, setPreferences] = useState({
    dailyGoal: 5,
    showTips: true
  });
  const [drills, setDrills] = useState<Drill[]>([]);

  const handleAddOne = () => {
    setRepetitions(repetitions + 1);
    console.log('Added one repetition');
  };

  const handleAddThree = () => {
    setRepetitions(repetitions + 1);
    setRepetitions(repetitions + 1);
    setRepetitions(repetitions + 1);
  };

  const handleToggleTips = () => {
    preferences.showTips = !preferences.showTips;
    setPreferences(preferences);
  };

src/App.tsx

Starter code — part 2 of 2

tsx
  const handleAddDrill = () => {
    drills.push(fixedDrills[0]);
    setDrills(drills);
  };

  const handleComplete = (id: string) => {
    setDrills(drills.map(drill => {
      if (drill.id === id) {
        drill.done = true;
      }
      return drill;
    }));
  };

  const handleRemove = (id: string) => {
    setDrills(drills.filter(drill => drill.id !== id));
  };

  return (
    <section>
      <h1>Practice Tracker</h1>
      <p>Repetitions: {repetitions}</p>
      <button type="button" onClick={handleAddOne}>Add 1</button>
      <button type="button" onClick={handleAddThree}>Add 3</button>
      <p>Daily goal: {preferences.dailyGoal}</p>
      <button type="button" onClick={handleToggleTips}>Toggle tips</button>
      {preferences.showTips && <p>Tips are visible.</p>}
      <button type="button" onClick={handleAddDrill}>Add drill</button>
      <ul>
        {drills.map(drill => (
          <li key={drill.id}>
            {drill.label}: {drill.done ? 'Done' : 'Not done'}
            <button type="button" onClick={() => handleComplete(drill.id)}>Complete</button>
            <button type="button" onClick={() => handleRemove(drill.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </section>
  );
}

export default function App() {
  return <PracticeTracker />;
}

Apply the repair rules

Repair the focused gaps

Complete the missing expressions. The declarations must remain before the early return; the two handlers must calculate immutable next state.

tsx
function PracticeTracker() {
  const [repetitions, setRepetitions] = ;
  const [preferences, setPreferences] = useState({
    dailyGoal: 5,
    showTips: true
  });

  if (!shouldRenderTracker) {
    return null;
  }

  const handleAddOne = () => {
    setRepetitions();
  };

  const handleToggleTips = () => {
    setPreferences();
  };
}

Compare with the corrected tracker

Corrected src/App.tsx

Reference code — part 1 of 2

tsx
import { useState } from 'react';

type Drill = {
  id: string;
  label: string;
  done: boolean;
};

const fixedDrills: Drill[] = [
  { id: 'drill-1', label: 'Trace a state update', done: false },
  { id: 'drill-2', label: 'Check Hook placement', done: false }
];

const shouldRenderTracker = true;

function PracticeTracker() {
  const [repetitions, setRepetitions] = useState(0);
  const [preferences, setPreferences] = useState({
    dailyGoal: 5,
    showTips: true
  });
  const [drills, setDrills] = useState<Drill[]>([]);

  if (!shouldRenderTracker) {
    return null;
  }

  const handleAddOne = () => {
    setRepetitions(previous => previous + 1);
    console.log('Added one repetition');
  };

  const handleAddThree = () => {
    setRepetitions(previous => previous + 1);
    setRepetitions(previous => previous + 1);
    setRepetitions(previous => previous + 1);
  };

  const handleToggleTips = () => {
    setPreferences(previous => ({
      ...previous,
      showTips: !previous.showTips
    }));
  };

Corrected src/App.tsx

Reference code — part 2 of 2

tsx
  const handleAddDrill = () => {
    setDrills(previous => {
      const nextDrill = fixedDrills.find(
        candidate => !previous.some(drill => drill.id === candidate.id)
      );
      return nextDrill ? [...previous, nextDrill] : previous;
    });
  };

  const handleComplete = (id: string) => {
    setDrills(previous => previous.map(drill => (
      drill.id === id ? { ...drill, done: true } : drill
    )));
  };

  const handleRemove = (id: string) => {
    setDrills(previous => previous.filter(drill => drill.id !== id));
  };

  return (
    <section>
      <h1>Practice Tracker</h1>
      <p>Repetitions: {repetitions}</p>
      <button type="button" onClick={handleAddOne}>Add 1</button>
      <button type="button" onClick={handleAddThree}>Add 3</button>
      <p>Daily goal: {preferences.dailyGoal}</p>
      <button type="button" onClick={handleToggleTips}>Toggle tips</button>
      {preferences.showTips && <p>Tips are visible.</p>}
      <button type="button" onClick={handleAddDrill}>Add drill</button>
      <ul>
        {drills.map(drill => (
          <li key={drill.id}>
            {drill.label}: {drill.done ? 'Done' : 'Not done'}
            <button type="button" onClick={() => handleComplete(drill.id)}>Complete</button>
            <button type="button" onClick={() => handleRemove(drill.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </section>
  );
}

export default function App() {
  return <PracticeTracker />;
}

Expected interface after the check sequence

Annotated Practice Tracker interface after adding four repetitions, hiding tips, and leaving one completed drill.

After the suggested interaction sequence, the count, preserved daily goal, hidden tips, and remaining completed drill provide visible checks of each update.

Verify locally

Run the local checks after saving src/App.tsx.

bash
npx tsc --noEmit
npm run lint
npm run dev
Expected output
> eslint .

> vite

  VITE vX.X.X  ready in XXX ms

  ➜  Local:   http://localhost:XXXX/
  ➜  press h + enter to show help

Exact Vite version, timing, and local URL vary. The type-check and lint commands should finish without diagnostics. The development server continues running until you stop it.

Verify and consolidate

Run the interaction sequence

Final verification

Use your repaired local tracker. Click Add 1 once, then Add 3 once. Toggle tips once. Click Add drill twice, complete Trace a state update, and remove Check Hook placement.

Confirm the two checking commands finish without diagnostics and inspect the browser console after Add 1. Report what you observed, including the Hook-rule repair you made. Do not rely on file upload or automatic inspection.

Report the local result

Report your verification in 3–6 short sentences: state where you placed the Hooks, whether npx tsc --noEmit and npm run lint passed, the final repetition count, what happened to the daily goal and tips, the remaining drill and its status, and the console message after Add 1.

Write at least 180 characters (0/180).

Summary

Practice Tracker checklist

  • Pass callbacks to event-handler props; do not invoke them during rendering.
  • Call Hooks unconditionally at the component top level and fix react-hooks/rules-of-hooks rather than suppressing it.
  • A handler reads its current state snapshot; functional updaters process queued changes from pending state.
  • Replace objects, arrays, and changed records immutably so unrelated fields and records remain intact.
  • Keep rendering and updater functions pure; put action-specific side effects such as logging in an event handler.

Tutorial complete

Congratulations! You completed: Update Component State from User Events

You repaired and locally verified a typed React component with predictable, immutable user-event updates.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate