Dual-Mode Reducer Components
Dual-mode reducer components in React are designed to adapt to varying ownership requirements, allowing for both controlled and uncontrolled states within a single component. In controlled mode, the parent manages the state via props, while in uncontrolled mode, the component maintains its own internal state. This approach prevents duplication of logic and ensures a consistent API by centralizing state resolution and behavior through a shared reducer. Best practices emphasize maintaining a single source of truth, avoiding mode switching post-initial render, and preventing conflicting updates to ensure predictable and maintainable component behavior.
As components become more reusable across large React codebases, they must adapt to different ownership requirements. In simple contexts, a component should manage its own state internally, uncontrolled usage. However, in more complex workflows, analytics pipelines, multi-step forms, or coordinated UI systems, the parent must own the state entirely and control usage. Building two separate components for these scenarios leads to divergence, duplicated logic, and maintenance overhead. The architectural goal is therefore to design one component whose reducer defines the behavioral rules, while the state can live either internally or externally, depending on the props provided.
Dual-mode reducer design
The mode of a reducer-driven component is determined by one question:
If a value (or checked, count, etc.) prop is provided, the component operates in controlled mode. If not, it works in uncontrolled mode. The reducer always describes the same state transitions; only the state owner changes.
In an uncontrolled mode:
The consumer omits the
valueprop.The component initializes local reducer state via
useReducer.Event handlers dispatch internal actions (
dispatchInternal).The rendered value is read directly from the internal reducer state.
In a controlled mode:
The consumer supplies a
valueprop.The component ignores its internal reducer state for rendering.
Event handlers must not update internal state; instead, they call
onChange(nextValue).State updates occur only when the parent re-renders with the new
value. ...