reactjsreact-selectreact-final-formreact-final-form-arrays

Conditional dropdowns with react-select in react-final-form initialized from the state


I'm using react-select and react-final-form for conditional dropdowns, where options for the second select are provided by a <PickOptions/> component based on the value of the first select (thanks to this SO answer).

Here is the component:

/** Changes options and clears field B when field A changes */
const PickOptions = ({ a, b, optionsMap, children }) => {
  const aField = useField(a, { subscription: { value: 1 } });
  const bField = useField(b, { subscription: {} });
  const aValue = aField.input.value.value;
  const changeB = bField.input.onChange;
  const [options, setOptions] = React.useState(optionsMap[aValue]);

  React.useEffect(() => {
    changeB(undefined); // clear B
    setOptions(optionsMap[aValue]);
  }, [aValue, changeB, optionsMap]);
  return children(options || []);
};

It clears the second select when the value of the first one changes by changeB(undefined). I've also set the second select to the first option in an array by passing initialValue. As I need to initialize the values from the state, I ended up with the following code:

initialValue={
  this.state.data.options[index] &&
  this.state.data.options[index].secondOption
    ? this.state.data.options[index]
        .secondOption
    : options.filter(
        option => option.type === "option"
      )[0]
}

But it doesn't work. Initial values from the state are not being passed to the fields rendered by <PickOptions/>. If I delete changeB(undefined) from the component, the values are passed but then the input value of the second select is not updated, when the value of the first select changes (even though the options have been updated). Here is the link to my codesandbox.

How can I fix it?


Solution

  • I was able to get this to work by taking everything that is mapped by the fields.map() section and wrapping it in it's own component to ensure that each of them have separate states. Then I just put the changeB(undefined) function in the return call of the useEffect hook to clear the secondary selects after the user selects a different option for the first select like so:

    React.useEffect(() => {
      setOptions(optionsMap[aValue]);
    
      return function cleanup() {
        changeB(undefined) // clear B
      };
    }, [aValue, changeB, optionsMap]);
    

    You can see how it works in this sandbox: React Final Form - Clear Secondary Selects.

    To change the secondary select fields, you will need to pass an extra prop to PickOptions for the type of option the array corresponds to. I also subscribe and keep track of the previous bValue to check if it exists in the current bValueSet array. If it exists, we leave it alone, otherwise we update it with the first value in its corresponding optionType array.

      // subscibe to keep track of previous bValue
      const bFieldSubscription = useField(b, { subscription: { value: 1 } })
      const bValue = bFieldSubscription.input.value.value
    
      React.useEffect(() => {
        setOptions(optionsMap[aValue]);
    
        if (optionsMap[aValue]) {
          // set of bValues defined in array
          const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);
    
          // if the previous bValue does not exist in the current bValueSet then changeB
          if (!bValueSet.some(x => x.value === bValue)) {
            changeB(bValueSet[0]); // change B
          }
        }
    
      }, [aValue, changeB, optionsMap]);
    

    Here is the sandbox for that method: React Final Form - Update Secondary Selects.

    I also changed your class component into a functional because it was easier for me to see and test what was going on but it this method should also work with your class component.