Skip to content

Lesson 4 — Hooks, reducer state, and derived data

Objective

Model tracker transitions with useReducer and calculate summaries without synchronized duplicate state.

Prerequisites

Lesson 3 — Props, composition, and component contracts. You should be able to lift state and pass callbacks.

Mental Model

State is a snapshot for one render. Dispatch queues a transition; it does not mutate the current closure. Reducers centralize related domain transitions and must be pure.

Concept

When many state variables move together, a reducer expresses the domain as state plus actions. Derived values should be computed during render, not stored and synchronized. This avoids stale duplicates and keeps the source of truth small.

Example

type State = { topics: Topic[] };

type Action =
  | { type: 'add'; topic: Topic }
  | { type: 'toggle'; id: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'add':
      return { topics: [...state.topics, action.topic] };
    case 'toggle':
      return {
        topics: state.topics.map((t) =>
          t.id === action.id ? { ...t, completed: !t.completed } : t
        ),
      };
  }
}

Real-World Usage

Trackers, carts, wizards, and editors where multiple fields share a lifecycle and transitions must be auditable.

Common Mistakes

  • Splitting one domain into many useState calls that drift out of sync.
  • Putting derived summaries back into state.
  • Reading state right after dispatch and expecting it to be updated.

Mini Lab

Task

Define typed state and discriminated actions for search, status changes, and sessions. Implement immutable transitions. Derive filtered topics, completed count, percentage, and minutes.

Constraints

  • Test the reducer without React.
  • Confirm the previous object is unchanged.
  • Invalid action payloads fail TypeScript.

Expected Result

A pure reducer passes its own tests, and component render functions derive all summary values from state without extra synchronization.

Hints

Store source facts only. Selectors calculate summaries. Keep transient form drafts local rather than adding them to the tracker reducer.

Knowledge Check

  • Why can event handlers observe stale state?
  • When is a reducer preferable to several state calls?
  • Why should derived data usually not live in an effect?

Challenge

You are asked to add undo/redo. What changes to the state shape and reducer are necessary, and why is a history array not a derived value?

Summary

Reducers model domain transitions; derived values are computed during render. State is a snapshot, and dispatch queues the next snapshot.

What Comes Next

Lesson 5 — Context and dependency boundaries