When using generic React components for UI elements like TextFields and Dropdowns, it's so nice when they adhere to the same API principles as built-in React elements like <input />
.
For example, built-in elements like <input />
, <select />
and <textarea />
all take value
and onChange
props as pairs for when you want to control their state closely (they then act as controlled components). If you don't specify a value
prop, the component will be uncontrolled.
What is an Uncontrolled React Component?
A React component is uncontrolled if you don't pass it a prop with the state of its value.
JS
<input name="email" type="email" defaultValue={user.email} />
A user can type in this uncontrolled input field but you can't easily update the text in the field programmatically (aside from the initial default value).
Uncontrolled inputs can be useful for quickly creating forms where the individual fields and controls don't interact much with each other, but it can be really annoying having to add value/onChange props later on when you need to.
What is a Controlled React Component?
A React component is controlled if you pass it the current state of its value
as a prop. You will then usually also specify an onChange
handler so that you can update the state when the user interacts with the component.
JS
const [email, setEmail] = useState('')const onChange = (e) => {setEmail(e.target.value)}const onClickReset = () => {setEmail('')}return (<><input name="email" type="email" value={email} onChange={onChange} /><button onClick={onClickReset}>Reset</button></>)
In the above example, when the email
state-variable changes, the actual text in the input field on the page will update accordingly.
There is also a reset-button that interacts with the text field. Clicking it causes the email-input to be cleared. This wouldn't be idiomatically possible if the component was uncontrolled.
Building a Custom TextField React Component
A common component to spot in a React codebase, is a custom Input
or TextField
component.
Let's see how we can build one that can display the number of letters that the user has typed:
JS
function TextField({ showLetterCount = false, ...rest }) {const [value, setValue] = useState('')const letterCount = value.lengthconst onChange = (e) => {setValue(e.target.value)}return (<div className="TextField"><input value={value} onChange={onChange} {...rest} />{showLetterCount && <p>{letterCount}</p>}</div>)}
This looks good and works but we can't control it easily from the outside. Right now, the TextField
component manages its state internally but we would like to be able to pass value
and onChange
from the outside.
JS
function TextField({ value, onChange, showLetterCount = false, ...rest }) {const letterCount = value.lengthreturn (<div className="TextField"><input value={value} onChange={onChange} {...rest} />{showLetterCount && <p>{letterCount}</p>}</div>)}
By simply moving value
and onChange
to the props, we can now use the TextField in a controlled manner:
JS
const [email, setEmail] = useState('')const onChange = (e) => {setEmail(e.target.value)}return <TextField name="email" type="email" value={email} onChange={onChange} />
But what if we want the controlled behaviour to be optional?
Making our TextField Component Optionally Controlled
To support both controlled and uncontrolled rendering, we need to accept { value, onChange }
in our props while also being able to manage our TextField's state internally.
Ideally our TextField component will emulate precisely how the built-in React <input />
element behaves:
- If an input is rendered with a
value
prop that is notundefined
, it is controlled - An input can accept a
value
prop or adefaultValue
prop but not both - An input isn't allowed to switch from being rendered as controlled to being rendered as uncontrolled at runtime (e.g. by starting out with a
value = ""
and then settingvalue = undefined
at some point.) - An uncontrolled input can still have an
onChange
handler
I've created a little demo on CodeSandbox that showcases these behaviours.
Here is how our TextField component might support all those requirements from above:
JS
function TextField({value: valueFromProps,onChange: onChangeFromProps,defaultValue,showLetterCount = false,...rest}) {// A component can be considered controlled when its value prop is// not undefined.const isControlled = typeof valueFromProps != 'undefined'// When a component is not controlled, it can have a defaultValue.const hasDefaultValue = typeof defaultValue != 'undefined'// If a defaultValue is specified, we will use it as our initial// state. Otherwise, we will simply use an empty string.const [internalValue, setInternalValue] = useState(hasDefaultValue ? defaultValue : '')// Internally, we need to deal with some value. Depending on whether// the component is controlled or not, that value comes from its// props or from its internal state.const value = isControlled ? valueFromProps : internalValueconst letterCount = value.lengthconst onChange = (e) => {// When the user types, we will call props.onChange if it exists.// We do this even if there is no props.value (and the component// is uncontrolled.)if (onChangeFromProps) {onChangeFromProps(e)}// If the component is uncontrolled, we need to update our// internal value here.if (!isControlled) {setInternalValue(e.target.value)}}return (<div className="TextField"><input value={value} onChange={onChange} {...rest} />{showLetterCount && <p>{letterCount}</p>}</div>)}
You can play around with this implementation on CodePen.
It's convenient to be able to render components without having to manage their state; and by following how React's built-in components behave, our component's props-API stays intuitive.
Once you've internalized this "hybrid control" pattern, it won't take you much longer to create your low-level React components but they will be a lot more enjoyable to use.
Links
- React (un)controlled TextField implementation on Codepen
- React (un)controlled inputs demo on Codepen