Learn how to implement Control Props to build flexible React components that bridge internal state management with external control via unidirectional data flow.
Previously in this course, we explored designing compound components to create expressive, modular UIs. While compound components solve the problem of structural coupling, they often require a way to synchronize state between the library consumer and the component's internal logic. This is where the Control Props pattern becomes essential.
In React, a "controlled" component is one where the data flow is dictated by props rather than internal state. Think of standard <input /> elements: if you pass value and onChange, the input is controlled. If you don't, it manages its own state internally.
The Control Props pattern allows your custom components to behave exactly like native inputs. It provides a "power user" API: by default, the component is self-contained and easy to use (uncontrolled), but if a consumer provides a value prop, the component yields control to the parent (controlled).
To implement this, you must treat your internal state as a fallback, ensuring that the source of truth is always the external prop when provided.
Let’s build a Toggle component. We want it to be simple to use by default, but allow parent components to force a state change—for example, if a "Reset" button needs to flip the toggle back to false.
JSXimport React, { useState, useEffect } from CE9178">'react'; const Toggle = ({ on, onChange, defaultOn = false }) => { // 1. Determine if we are in "controlled" mode const isControlled = on !== undefined; // 2. Internal state for uncontrolled mode const [internalOn, setInternalOn] = useState(defaultOn); // 3. The source of truth const state = isControlled ? on : internalOn; const toggle = () => { const nextState = !state; // 4. Notify the parent if in controlled mode, // otherwise update internal state if (!isControlled) { setInternalOn(nextState); } onChange?.(nextState); }; return ( <button onClick={toggle}> {state ? CE9178">'ON' : CE9178">'OFF'} </button> ); };
state variable acts as a computed property. We never modify props directly; we only communicate intent via onChange.on prop (uncontrolled) or provide it to synchronize the toggle with their own complex state (controlled).onChange execution: We execute onChange regardless of the mode, allowing the parent to react to interactions even when they aren't explicitly controlling the state.onResetIn our current project—the large-scale dashboard we've been refactoring—we have a filter panel. Modify the Toggle component above to accept an optional onReset prop. When a reset occurs, the component should update its internal state to the defaultOn value even if it is currently in controlled mode.
Hint: Use a useEffect hook to sync defaultOn if the component is uncontrolled, or simply provide a public method via useImperativeHandle if you need to trigger an external reset.
undefined check: Never rely on a truthy/falsy check for props like value or on. 0 or false are valid values. Always check prop !== undefined.internalOn and calling onChange in a way that causes the parent to re-render and pass the same value back, leading to unnecessary cycles. Always keep your conditional logic clean: if isControlled, setInternalOn is never called.onChange: If you provide a controlled component, failing to provide an onChange handler essentially makes your component read-only. Always provide a warning or ensure your internal logic handles the absence of the handler gracefully (as shown with onChange?.()).| Pattern | Source of Truth | Best For |
|---|---|---|
| Uncontrolled | Internal useState | Simple, self-contained UI |
| Controlled | Parent state | Syncing across components |
| Control Props | Hybrid (Prop > State) | Reusable library components |
By implementing Control Props, you move your components from "black boxes" that hide their state to "collaborative primitives" that integrate seamlessly into any application architecture. This is a foundational step toward building the headless UI architectures we will discuss in the next lesson.
We will take the lessons learned here and apply them to Headless UI Architectures, decoupling our state management logic entirely from the DOM elements to create truly reusable, accessible primitives.
Master State Colocation to stop unnecessary re-renders. Learn to move state as close as possible to its consumption point for high-performance React apps.
Read moreStop prop-drilling modal visibility. Learn to build a global modal system using Context API and state to trigger UI overlays from anywhere in your app.
Implementing Control Props
Final Project Audit & Optimization
Advanced Hook Patterns
Managing Global State with Zustand/Redux
Testing Performance-Critical Components
Static Site Generation (SSG) Patterns
Internationalization (i18n) Architecture
Accessibility (a11y) in Advanced Components
Managing Third-Party Integrations
Advanced Form Handling
Using Portals for UI Overlays
Implementing Virtualized Lists
Building Design System Primitives
Managing Large-Scale Data Fetching
Micro-Frontends with React
Security Best Practices in React
Advanced Ref Usage
Memoization Pitfalls
Mastering React Patterns for Scalability
Advanced TypeScript with React