🎛️ Control Props in React: Letting Users Take the Wheel
Let’s talk about a powerful pattern that will take your component APIs to the next level of flexibility: Control Props.
Imagine this: you’re building a reusable component. You want it to work out of the box, but you also want to give developers the option to control its behavior completely—from the outside.
Sound familiar?
It should. You’ve already seen this pattern in action a thousand times:
<input value={value} onChange={handleChange} />
This is a controlled input element. You provide the value, and React calls your onChange to “suggest” a new one.
Let’s take that same concept… and apply it to any reusable component.
Controlled inputs are the basis of most React forms:
function MyCapitalizedInput() {const [capitalizedValue, setCapitalizedValue] = useState("")return (<inputvalue={capitalizedValue}onChange={(e) => setCapitalizedValue(e.target.value.toUpperCase())}/>)}
This is a classic example of using control props (value and onChange). The input doesn’t manage its own state anymore — you do.
Why is this so powerful?
Let’s say we’ve built a <Toggle /> component. It’s a simple on/off switch.
Here’s the typical version where the component manages its own state:
function Toggle() {const [on, setOn] = useState(false)return <button onClick={() => setOn(!on)}>{on ? "On" : "Off"}</button>}
Nice and simple. But what if someone wants to control that on state from the outside?
Now we need to implement control props:
function Toggle({ on: controlledOn, onChange }) {const [internalOn, setInternalOn] = useState(false)const isControlled = controlledOn !== undefinedconst on = isControlled ? controlledOn : internalOnfunction toggle() {const newState = !onif (isControlled) {onChange?.(newState)} else {setInternalOn(newState)onChange?.(newState)}}return <button onClick={toggle}>{on ? "On" : "Off"}</button>}
If on is passed in → the component is controlled
If on is undefined → the component manages state internally
When toggled →
onChange with the suggested next stateonChangeThis gives users two options:
<Toggle /> like a normal self-contained componenton and onChange to fully manage its state externallyWhat if we want two components to always reflect the same value?
function MyTwoInputs() {const [value, setValue] = useState("")function handleChange(e) {setValue(e.target.value)}return (<><input value={value.toUpperCase()} onChange={handleChange} /><input value={value.toLowerCase()} onChange={handleChange} /></>)}
Using control props here makes it possible to synchronize behavior between components. This is especially powerful when building design systems or shared UI libraries.
Back to our <Toggle />:
Your exercise is to make it behave like a controlled component. Specifically, you should:
on proponChange callbackon is not providedonChange with the proposed next valueThis will give consumers full flexibility:
on and onChange to take control themselvesThe Control Props pattern is used in many popular UI libraries, including:
downshift for autocomplete & dropdowns@radix-ui/react-select for customizable select menusThese libraries rely on control props to give users the power to integrate deeply with their own state and data.
The Control Props pattern:
| ✅ Feature | 🔍 Benefit |
|---|---|
| User-controlled state | Lets devs hook your component into their own app logic |
| Bidirectional sync | Great for syncing values across components |
| Declarative flexibility | Makes your API more powerful without adding complexity |
If you’re building a reusable component and want to let developers fully manage or sync state, then the Control Props pattern is exactly what you need.
Control Props is more than just a fancy name — it’s about making components flexible enough to be used in real-world, complex situations while still being dead simple for the easy cases.
The best APIs do both. 💪