Transitions as explicit decisions
A reducer receives state and action and returns next state. The convention of an action with type and additional data helps make intent readable, but React assigns no business meaning to words such as archived or closed. In the example, archived changes a boolean in a local record because that is the implemented branch.
The reducer neither closes a connection nor sends confirmation. When reviewing an action, identify its entry condition, the object allowed to change and fields that must remain. Use simple props and handlers while they suffice; introducing a reducer should clarify existing transitions rather than invent an unnecessary global system.
The result is complete state
The value returned by a reducer replaces previous state. If state was {title, status} and a branch returns only {status: saved}, React does not recover title. Explicitly include fields to preserve, for example with spread, and copy changed levels without mutating earlier objects.
A reducer that writes into state itself and returns it can also interfere with identity comparison used to avoid work. Do not use that behavior as an update mechanism. The example creates a new array and new object for the selected record while leaving other objects intact.
Review should assess every branch and result, including unrecognized actions.
Dispatch, initialization and purity
dispatch receives the action; it neither returns calculated state nor rewrites the currently executing handler’s snapshot. Its identity is stable between the instance’s renders, but that does not make state values mutable. For initialization through a function, useReducer(reducer, seed, buildInitial) uses the third argument as initializer.
Writing buildInitial(seed) in the second argument evaluates the expression when the component is called, even when that initial value is no longer used. Reducer and initializer must be pure. Development checks can evaluate them again to reveal mutations and effects; they must not trigger external writes or messages on each evaluation.
Distribute state and intent through the tree
The example keeps useReducer in ReviewQueue and supplies rows and dispatch through two contexts. QueueBody reads both because it displays records and triggers actions. Another descendant might need only reading or only dispatch.
Separation clarifies dependencies but does not guarantee that components never render again for other reasons. Context is a way to carry values through the tree; it does not replace the decision about state ownership. The provider must surround relevant consumers.
A consumer outside that boundary does not automatically receive the data merely by sharing a file or using a similar name.
Nearest provider and fallback
useContext selects the closest matching provider above the component. A nested provider can override value for its subtree without automatically merging with outer values. If no matching provider exists above, the createContext default is used.
A provider supplying undefined still exists and its consumer receives undefined; the default does not normalize that value. Panel also cannot read a provider it only returns around its children: that provider is below the reading. In the example, null deliberately signals a missing provider and QueueBody reports that error rather than continuing under invalid assumptions.
Identity of context and value
Provider and consumer must use the same object created by createContext. Two module copies can create different objects with the same name; matching names do not make them interchangeable. Investigate this boundary when a consumer receives fallback despite an apparently present provider.
Then distinguish context identity from its value identity. A new {user, login} object on each render compares as different and can update consumers even when underlying data has not changed. memo does not prevent a necessary context update.
Stabilizing objects and callbacks can be useful optimization, but must preserve correct dependencies and be assessed against observed work.
Review the complete operation
Start with previous state and the received action. Calculate each branch’s result and verify that required fields remain. Then follow that result to the provider and correct consumer.
If the interface stays stale, distinguish mutation of the same object, missing provider, duplicated context and merely reading an older snapshot in the handler. The answer is not always more state or another provider. The example is deliberately local: archived means a decision stored in this instance’s memory.
A real integration would need to represent requests, confirmation and failure separately, keeping the reducer responsible for calculating transitions rather than executing those effects.
import {createContext, useContext, useReducer} from 'react';
const RowsContext = createContext(null);
const DispatchContext = createContext(null);
function reducer(rows, action) {
switch (action.type) {
case 'archived':
return rows.map(row => row.id === action.id
? {...row, archived: true} : row);
default:
throw new Error('Unknown action');
}
}
function QueueBody() {
const rows = useContext(RowsContext);
const dispatch = useContext(DispatchContext);
if (rows === null || dispatch === null) throw new Error('Queue provider missing');
return <ul>{rows.map(row => <li key={row.id}>
{row.title}: {row.archived ? 'Archived locally' : 'Open'}
<button type="button" disabled={row.archived}
onClick={() => dispatch({type: 'archived', id: row.id})}>Archive locally</button>
</li>)}</ul>;
}
// initialRows: stable unique IDs, string titles, boolean archived.
export default function ReviewQueue({initialRows}) {
const [rows, dispatch] = useReducer(reducer, initialRows);
return <RowsContext value={rows}>
<DispatchContext value={dispatch}><QueueBody /></DispatchContext>
</RowsContext>;
}Archive locally sends an action with an id. The reducer returns a new array and copies the changed object; it makes no network request. Contexts distribute state and dispatch to QueueBody.
The <Context value> syntax uses React 19. The example does not demonstrate authorization or remote persistence.
Common pitfalls
Returning a partial object expecting a merge; mutating received state; reading dispatch’s return as state; placing the provider below the consumer; creating different contexts with the same name; confusing memoization with correctness.
Related topics: State structure and component identity · Effects, refs and external synchronization · Performance, transitions and rendering decisions
A reducer interprets an action and returns next state without effects. Context carries a value through the tree using provider identity and proximity. Neither mechanism by itself confirms an operation in an external service.
Reference: Reducer contract · React 19.3.0; JSX