Controlled HTML select element in React has weird default UX
When you create a controlled input
in React with a
blank value
, the UI will show nothing in the textbox.
function Input({ value, onChange }) {
return (
<input type="text" value={value} onChange={e => onChange(e.target.value)} />
);
}
What about select
elements?
function Select({ value, onChange, children }) {
return (
<select value={value} onChange={e => onChange(e.target.value)} children={children} />
);
}
... in use ...
const [value, setValue] = React.useState('');
<Select value={value} onChange={setValue}>
<option value="ford">Ford</option>
<option value="toyota">Toyota</option>
</Select>
If you pass this blank value
into a
Select
, the UI will look as if Ford
is
selected.
And to make things more confusing (if you as a developer know that
it only looks like Ford
is
selected), you open the dropdown and select Ford
again,
the onChange
handler is not going to get called. I am
not sure exactly why that happens but it points at the UI and React
state being out of sync.
Basically, weird things happen if you pass in a value
to select
that isn't a value of one of
its option
s.
The right situation for a select
element is that it
should always have a valid value. There are two ways I can think of
dealing with this: 1) hope every caller of Select
picks
a valid initial value or 2) use an effect to trigger the passed in
onChange
with a valid default value. Here's what the
latter could look like.
function Select({ value, onChange, children }) {
React.useEffect(() => {
if (!value || !children) return;
const values = React.Children.map(children, (i) => i ? i.props.value : null).filter(Boolean);
if (values.length && !values.includes(value)) {
onChange(values[0]);
}
}, [value]);
return (
<select value={value} onChange={e => onChange(e.target.value)} children={children} />
);
}
This technique is what I ended up using in DataStation. The effect is only ever called once to initialize the value. It gets rid of all the out-of-sync weirdness if a blank value is passed in.
The only thing I'm not sure of is that children
is not
included in the list of the effect's dependencies. I assume React
takes care of re-rendering the entire component if children change
but I am not sure.
I'm curious to hear what you think or how you've tackled this.
In this post we talk through the weird things that can happen if you don't initialize <select> elements correctly in React and how to work around this situation#javascript #React https://t.co/AoI3rEXWFe pic.twitter.com/vHR1605mhz
— DataStation (@multiprocessio) June 25, 2021
With questions, criticism or ideas, email or Tweet me.
Also, check out DataStation and dsq.