šļø 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 stateonChange
This 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. šŖ
Quick Links
Legal Stuff
Social Media