Course outline

React / Coordinated application behavior / Lesson 9

Reducers, context, and state ownership

Model coherent transitions with pure reducers and share scoped values without turning context into an unexplained global store.

About 4 min readContent reviewed
On this page

Lesson goals

  • Put state at its meaningful owner
  • Preserve an invariant with one transition
  • Review lifetime and testable boundaries

Put state at its meaningful owner

Lift shared state to the nearest owner that must coordinate it, rather than duplicating competing copies in sibling components. Keep local transient state local when no other part needs it. A clear owner makes callbacks and derived values easier to trace. Context can transport a value through a subtree without manually passing every intermediate prop, but it does not choose ownership or persistence policy for you. Identify the provider’s lifetime and which consumers require updates. A context value changing can update its consumers, so a giant frequently changing value can create unnecessary coupling between unrelated features.

A reducer describes a state transition

useReducer passes the current state and an action to a pure reducer that returns the next state. Actions should name domain events, such as itemAdded, rather than exposing arbitrary internal mutation instructions everywhere. Validate invariants before returning a changed result and preserve the input snapshot. The reducer must not perform requests, read random values, or mutate external objects. Event handlers can create stable ids or coordinate effects at their owning boundary, then dispatch a complete action. A reducer does not automatically make all transitions correct; its advantage is putting the rules in one explicit, testable place.

Preserve an invariant with one transition

The first example models a bounded seat counter with actions that cannot move it below zero. The reducer returns the old state when a decrement is disallowed. Repeated actions therefore preserve the nonnegative invariant without coordinating two setters. The example is a local training counter, not a booking system: a server must still enforce real capacity and authorization. Pure reducer tests can examine zero, positive values, and unknown actions without rendering a browser. UI tests then verify that button activation dispatches the right intent and the resulting count is communicated correctly.

jsx Component module — requires the course React host
import { useReducer } from "react";
export function reducer(count, action) { if (action.type === "add") return count + 1; if (action.type === "remove") return Math.max(0, count - 1); return count; }
export default function App() { const [count, dispatch] = useReducer(reducer, 0); return <main><h1>Seats: {count}</h1><button type="button" onClick={() => dispatch({ type: "add" })}>Add</button><button type="button" onClick={() => dispatch({ type: "remove" })}>Remove</button></main>; }
Expected output
Initial: Seats: 0. Remove at zero: Seats: 0. Add: Seats: 1.

One pure transition enforces the lower bound. Remove at zero leaves the value unchanged.

Provide a scoped read value

The second example uses createContext and a provider to supply a theme label to a nested consumer. The .Provider syntax is compatible with the tested React 18 baseline and remains valid in React 19, which also offers newer provider syntax. A default context value is used only when no matching provider exists, not when a provider explicitly supplies undefined. Keep required-provider expectations explicit rather than silently continuing with a misleading default. Split contexts or state boundaries when their consumers and update rates differ; context is transport, not a reason to merge unrelated domain state into one object.

jsx Component module — requires the course React host
import { createContext, useContext } from "react";
const Theme = createContext("default");
function Badge() { return <p>Theme: {useContext(Theme)}</p>; }
export default function App() { return <main><h1>Scoped context</h1><Theme.Provider value="night"><Badge /></Theme.Provider></main>; }
Expected output
Scoped context
Theme: night

The nearest provider supplies night; the consumer does not use the default because a matching provider exists.

Review lifetime and testable boundaries

Test reducer transitions independently and context behavior within the intended provider tree. A provider remount can reset the state it owns, and nested providers can intentionally shadow outer values. Inspect these boundaries when a consumer seems stale or resets unexpectedly. Do not use context to bypass a needed component API simply because passing one prop feels repetitive. A good design makes data flow discoverable and permits isolated tests with small providers. Persisting or synchronizing the state with storage remains a separate external boundary, including schema, migration, failure, and privacy decisions that a reducer or context cannot supply automatically.

Common errors and fixes

Sibling values disagree

Why it happens
The same shared fact has independent state copies.
How to fix it
Lift the authoritative state to a coordinating owner.
Prevent it next time
Identify one source of truth per fact.

A reducer repeats an external operation

Why it happens
It performs side effects while calculating next state.
How to fix it
Keep transitions pure and own effects separately.
Prevent it next time
Run pure transition tests under repeated calls.

A provider default does not replace undefined

Why it happens
A matching provider explicitly supplied undefined.
How to fix it
Correct the provider value or enforce its contract.
Prevent it next time
Test missing provider separately from missing value.

Check your understanding

1. Design actions for adding and removing a cart item without exposing arbitrary state replacement.

Show worked solution

Use events such as itemAdded with an id and quantity, and itemRemoved with an id. Let the reducer enforce quantity and ownership rules through immutable transitions. This centralizes domain decisions while callers communicate intent rather than rewriting the entire cart structure.

2. When should a frequently changing text draft stay outside a shared theme context?

Show worked solution

When theme consumers do not need draft updates. Keep the draft with its form owner and transport only values that belong to each context’s responsibility. This limits coupling and avoids making unrelated consumers observe a rapidly changing aggregate value.

Key takeaways

  • Ownership precedes transport.
  • Reducers calculate pure transitions.
  • Context has a provider scope and lifetime.

Official references