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.
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.
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>; }
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.
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>; }
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.