javascripthtmlreactjsdrop-down-menu

React: 'selected' attribute not working with multiple <select> elements


I have a React component that has multiple <select> (not react-select) elements. Only one <select> is rendered based on one of the props:

class SelectGroup extends Component {

...

  render () {

    let activeSelect

    switch (this.props.foo) {
      case 'select1':
        activeSelect = <select id="select1">
          <option value="1a" selected>1 A</option>
          <option value="1b">1 B</option>
          <option value="1c">1 C</option>
        </select>
        break
      case 'select2':
        activeSelect = <select id="select2">
          <option value="2a" selected>2 A</option>
          <option value="2b">2 B</option>
          <option value="2c">2 C</option>
        </select>
        break
      ...
    }

    return activeSelect
  }
...
}

(For our purposes, assume that SelectGroup is nested inside another component, which changes the prop foo when a button is clicked.)

What I want is for the first option for each <select> to be selected by default whenever we change which <select> is being rendered (whenever the prop foo is changed). As you can see, I gave the first option of each <select> the selected attribute which, according to this documentation, should make it the default option. However, that's not the behavior I'm seeing. Here's what happens:

  1. select1 is being displayed. I choose option 1b from the dropdown.
  2. I click the button to switch to select2.
  3. I expect for 2a to be selected because it has the selected attribute. However, 2b is selected instead.
  4. I select 2c from the dropdown.
  5. I click the button to switch back to select1.
  6. I expect for 1a to be selected. However, 1c is selected instead.

It seems to be completely ignoring the selected attribute and instead it looks like it's selecting the option from the list that corresponds to whichever option was selected before changing which <select> is rendered.

Oddly, if I remove the selected attribute from all <select> elements except select1, then select1 will behave as expected: when I switch to select1, it will always display option 1a regardless of what was selected before, but the option displayed by select2 will continue to depend on whatever was chosen before switching to it. So it's as if selected only works if it is only used once, but I don't understand why that is, since it is only being applied to one option per <select>.

How can I get the first option to be selected whenever I switch between which <select> is being rendered?


Solution

  • Try adding a unique key to each select:

        switch (this.props.foo) {
          case 'select1':
            activeSelect = <select key="select1" id="select1">
              <option value="1a" selected>1 A</option>
              <option value="1b">1 B</option>
              <option value="1c">1 C</option>
            </select>
            break
          case 'select2':
            activeSelect = <select key="select2" id="select2">
              <option value="2a" selected>2 A</option>
              <option value="2b">2 B</option>
              <option value="2c">2 C</option>
            </select>
            break
          ...
    

    This will force a complete removal & subsequent append when the condition flips. What is happening is most likely when you switch from one to the other, react's VDOM kicks in, and it modifies the existing select rather than replaces it. Due to (quite weird and archaic) DOM behaviors, the selected value of the old select is retained.

    By using a unique key it will force React to remove and recreate the whole node completely, throwing away any browser state about the selected value that exists in memory (which is outside of Reacts purview and is a rare misalignment with React's mental model) in the process.

    What you are doing is a bit weird btw -- there are almost certainly better ways but I don't know enough about your use case. You probably want to instead conditionally render them inline, which means in the internal representation of the component held by React, they are 2 separate nodes:

    return <>
        {this.props.foo === "select1" && 
           <select id="select1">
              <option value="1a" selected>1 A</option>
              <option value="1b">1 B</option>
              <option value="1c">1 C</option>
            </select>
        }
        {this.props.foo === "select2" && 
           <select id="select1">
              <option value="1a" selected>1 A</option>
              <option value="1b">1 B</option>
              <option value="1c">1 C</option>
            </select>
        }
    
    </>
    

    You might think "how is this any different". Well, the inactive ones will render false (which renders nothing). But that it renders "false" means that node is represented separately as that value and not stuffed into one, which is why reacts vdom was treating it as the same root element.