DataStation Documentation Blog Community Subscribe
Blog

Controlled HTML select element in React has weird default UX

Published on by

DataStation is an open-source data IDE for developers. It allows you to easily build graphs and tables with data pulled from SQL databases, logging databases, metrics databases, HTTP servers, and all kinds of text and binary files. Need to join or munge data? Write embedded scripts as needed in Python, JavaScript, Ruby, R, or Julia. All in one application.

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 options.

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.

Questions? Feedback? Feel free to reach the author at phil@multiprocess.io.

Or to stay in the loop about future posts!