Search code examples
javascriptreactjsreact-reduxredux-formreact-select

Created select option from state not updating form json unless clicked


I have a redux-form and react-select working together. react-select has an the ability to create a new select option on the fly by typing into the select text field.

In my example you will see that I have updated the view so that the newly created option is highlighted as selected and appears in the select field.

However, this does not update my form json unless I click the newly created option that looks as though its already selected.

To replicate: type in select box - you will notice that the new created option is highlighted and select field updated. But you will also see that the json values remain unchanged. Click on the newly created option, you will see the values change.

Codesandbox example: https://codesandbox.io/s/k2v2922nq5

import React from "react";
import { Field, reduxForm, FieldArray } from "redux-form";
import TextField from "material-ui/TextField";
import { RadioButton, RadioButtonGroup } from "material-ui/RadioButton";
import Checkbox from "material-ui/Checkbox";
import SelectField from "material-ui/SelectField";
import MenuItem from "material-ui/MenuItem";
import asyncValidate from "./asyncValidate";
import validate from "./validate";
import CreatableSelect from "react-select/lib/Creatable";
const CustomStyle = {
  option: (base, state) => ({
    ...base,
    display: "inline",
    marginRight: "10px",
    backgroundColor: state.isSelected ? "#00285C" : "#eee",
    cursor: "pointer"
  }),
  menuList: () => ({
    padding: 10,
    display: "inline-flex"
  }),
  menu: () => ({
    position: "relative"
  })
};

const createOption = (label: string) => ({
  label,
  value: label.toLowerCase().replace(/\W/g, "")
});
class LastNameSelectInput extends React.Component {
  constructor(props) {
    super(props);
  }

  state = {
    value: this.props.options[0],
    options: this.props.options
  };
  handleCreate = (inputValue: any) => {
    this.setState({ isLoading: true });
    setTimeout(() => {
      const { options } = this.state;
      const newOption = createOption(inputValue);
      this.setState({
        isLoading: false,
        options: [...options, newOption],
        value: newOption
      });
    }, 1000);
  };
  render() {
    const { input, options } = this.props;
    return (
      <div>
        <style>
          {`.react-select__dropdown-indicator,.react-select__indicator-separator {
          display: none;
        }`}
        </style>
        <CreatableSelect
          classNamePrefix="react-select"
          options={this.state.options}
          menuIsOpen={true}
          onChange={value => {
            let newValue = input.onChange(value);
            this.setState({ value: newValue });
          }}
          onBlur={() => input.onBlur(input.value)}
          onCreateOption={this.handleCreate}
          value={this.state.value}
          styles={CustomStyle}
          isClearable
        />
      </div>
    );
  }
}

const MaterialUiForm = props => {
  const { handleSubmit, options, pristine, reset, submitting } = props;

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <Field name="firstName" component={LastNameSelectInput} {...props} />
      </div>
    </form>
  );
};

export default reduxForm({
  form: "MaterialUiForm", // a unique identifier for this form
  validate,
  asyncValidate
})(MaterialUiForm);

Solution

  • So when you create a new option, you're setting the React component state, but not doing anything to let redux-form know, which uses the Redux store as its state. This is always a big source of confusion (React component state vs. Redux store).

    The solution is to let redux-form know that we want to select our newly created option as well. This can be done by dispatching the onChange manually.

    I forked your sandbox and added the manual dispatch.

    Notable changes (omitting existing code for brevity):

    handleCreate

    // takes the input to fire dispatch upon
    handleCreate = input => (inputValue: any) => {
    
    ...
    this.setState(...);
    input.onChange(newOption); // manually dispatching the change to let redux-form know
    

    render

    // pass a ref to the input in handleCreate
    onCreateOption={this.handleCreate(input)}
    

    redux-form let's you manually trigger change using field name as well but a bound input is the most preferable.