Search code examples
reactjsselectjsxdropdowncontrolled-component

How to correctly update <select multiple ...> in React?


In a react app, you should control a <select>. It's real easy with a single select:

const ExampleSelect = () => {
  const [value, setValue] = React.useState()
  return (
    <select
      value={value}
      onChange={(e) => setValue(e.currentTarget.value)}
    >
      <option value='foo'>Foo</option>
      <option value='bar'>Bar</option>
      <option value='baz'>Baz</option>
    </select>
  )
}

But when you add the multiple prop to a <select> things get weird. Functions passed to onChange that iterate through the options and take actions based on which ones have option.selected === true only seem to work on desktop -- in Safari on iOS, a select with multiple === true seems impossible to control.

I feel like this is a really basic question, but I can't find anything on it. I'm not looking to install a whole library that comes with pre-styled components just to be able to select multiple options in a <select> inside a react app.

Here are a few onChange functions I've tried:

type Setter = React.Dispatch<React.SetStateAction<string[] | undefined>>
type SelectChange = React.ChangeEvent<HTMLSelectElement>

export const handleMultiSelectChange = <S extends Function = Setter>(
  value: string[] | undefined, setter: S
) => (
    e: SelectChange
  ): void => {
    const v = e.currentTarget.value
    if (value && value.length > 0) {
      const index = value.indexOf(v)
      if (index >= 0) {
        const newValue = value.filter((opt) => opt !== v)
        setter(newValue)
      } else {
        setter([...value, v])
      }
    } else {
      setter([v])
    }
  }
type Setter = React.Dispatch<React.SetStateAction<string[] | undefined>>
type SelectChange = React.ChangeEvent<HTMLSelectElement>

export const handleChangeMultiple = <S extends Function = Setter>(
  value: string[] | undefined = [], setter: S
) => (
    event: SelectChange
  ): void => {
    const {options} = event.currentTarget
    for (let i = 0, l = options.length; i < l; i += 1) {
      const option = options[i]
      if (option.selected) {
        alert(value.includes(option.value))
        if (value.includes(option.value)) {
          const index = value.indexOf(option.value)
          value.splice(index, 1)
        } else {
          value.push(option.value)
        }
      }
    }
    setter(value)
  }

(those ones are used like this:)

const ExampleSelect = () => {
  const [value, setValue] = React.useState()
  return (
    <select
      multiple
      value={value}
      onChange={handleChangeMultiple(value, setValue)}
    >
      <option value='foo'>Foo</option>
      <option value='bar'>Bar</option>
      <option value='baz'>Baz</option>
    </select>
  )
}

(though that's not working correctly for me)


Solution

  • For select, e.target gives you options array and you can loop thru it getselected option. Use map to grab the actual values and store them in your state.

    select

       <select
            onChange={handleChangeNormalSelect}
            multiple
            value={val}
            options={options}
          >
            {options.map(item => {
              return <option value={item.value}>{item.label}</option>;
            })}
          </select>
    

    onChange

    ...
    const [val, setVal] = useState([]);
    ...
    const handleChangeNormalSelect = e => {
        const updatedOptions = [...e.target.options]
          .filter(option => option.selected)
          .map(x => x.value);
        console.log("updatedOptions", updatedOptions);
        setVal(updatedOptions);
      };
    ...
    

    Working code sample(html multi select) is here