Create select dropdown in react

Learn how to create select box in react with option to select multiple values.

Maintaining native select tag is some time difficult in react because it cannot be controlled easily, that is why we will create custom select box in which we will have complete control over it.

Following is the list of props with which we will create our component.

options: PropTypes.arrayOf(
    PropTypes.shape({
    value: PropTypes.any.isRequired,
    label: PropTypes.string.isRequired
    })
).isRequired,
name: PropTypes.string.isRequired,
/* Render multi dropdown */
multi: PropTypes.bool,
onChange: PropTypes.func,
onBlur: PropTypes.func,
prefillValue: PropTypes.any,
label: PropTypes.node,
className: PropTypes.string,
labelClassName: PropTypes.string,
value: PropTypes.any,
helperText: PropTypes.string,
innerRef: PropTypes.instanceOf(Element),
error: PropTypes.bool,
disabled: PropTypes.bool,
disableOptionsByValue: PropTypes.arrayOf(PropTypes.any)

These are some of the minimum props that I require to create a functional component with some better control.

options will be an array of objects with label and value which will be available as different options.

disableOptionsByValue will disable certain options whose value is passed in an array.

Our component is made of three different parts.

  • Label: This will be the label of the component.
  • Select box: Our actual select drop down below the label.
  • Helper text: This will display helpful messages such as error or some other thing.

To create our component we will some extra packages which will ease our life in development.

  • prop-types: To validate the props we are going to receive.
  • classnames: This package helps us to use CSS classes as javascript ojbects. We just need to name our CSS file as filename.module.css.
  • lodash: Set of helpful functions to speed the development.
  • react-select: We are going to use this instead of native select tag of HTML because it is easier to handle and extend.

Following is the folder structure of our select component.
React Select Folder Structure

Now as we have good idea of how our component will be made. Let us begin creating it.

First, import all the required packages.

import React, { Component } from "react";
import PropTypes from "prop-types";
import ReactSelect from "react-select";
import cx from "classnames";
import { map, isEqual } from "lodash";
import styles from "./index.module.css";

class Select extends Component {
  //Other codes will go here
}

export default React.forwardRef((props, ref) => {
  return <Select {...props} innerRef={ref} />;
});

As we need to maintain the state to store the selected options, we are creating a stateful component using class.

We are also forwarding ref from the parent to give better control of the component.

Validate the props at the beginning before doing anything else and set the defaults in case we did not receive the expected props.

  static propTypes = {
    options: PropTypes.arrayOf(
      PropTypes.shape({
        value: PropTypes.any.isRequired,
        label: PropTypes.string.isRequired
      })
    ).isRequired,
    name: PropTypes.string.isRequired,
    /* Render multi dropdown */
    multi: PropTypes.bool,
    onChange: PropTypes.func,
    onBlur: PropTypes.func,
    prefillValue: PropTypes.any,
    label: PropTypes.node,
    className: PropTypes.string,
    labelClassName: PropTypes.string,
    value: PropTypes.any,
    helperText: PropTypes.string,
    innerRef: PropTypes.instanceOf(Element),
    error: PropTypes.bool,
    disabled: PropTypes.bool,
    disableOptionsByValue: PropTypes.arrayOf(PropTypes.any)
  };

  static defaultProps = {
    className: "",
    size: "default",
    disabled: false,
    disableOptionsByValue: []
  };

Check if default value is passed or not and then store it in the state. I will be doing it inside the constructor, you can store it in the state directly.

constructor(props) {
  super(props);
  this.state = {
    value: props.value || props.prefillValue || ""
  };
}

Now pull all the props that we have received and create different classes for the different parts of the component.

render() {
    const {
      className,
      multi,
      options,
      labelClassName,
      label,
      innerRef,
      error,
      helperText,
      disableOptionsByValue,
      disabled,
      ..._props
    } = this.props;

    let { value } = this.state;

    const _className = cx(className, styles.container, {
      [styles.hasError]: error
    });

    const _labelClassName = cx(labelClassName, styles.label, {
      [styles.error]: error
    });

    const _helperTextClassName = cx(styles.helperText, {
      [styles.error]: error
    });

    const _selectErrorStyle = error
      ? {
          control: (provided, state) => {
            const style = {
              ...provided,
              borderColor: "red",
              "&:hover": {
                borderColor: "red"
              }
            };

            if (state.isFocused) {
              return {
                ...style,
                boxShadow: "0 0 0 1px red"
              };
            } else {
              return {
                ...style
              };
            }
          },
          clearIndicator: provided => ({
            ...provided,
            color: "red"
          }),
          dropdownIndicator: provided => ({
            ...provided,
            color: "red"
          })
        }
      : {};

    return (
      <div className={_className}>
        {label ? <label className={_labelClassName}>{label}</label> : null}

        <ReactSelect
          {..._props}
          styles={_selectErrorStyle}
          options={options}
          isMulti={multi}
          onChange={multi ? this.handleMultiChange : this.handleOnChange}
          value={value}
          onBlur={this.handleOnBlur}
          ref={innerRef}
          isOptionDisabled={({ value }) =>
            disableOptionsByValue.includes(value)
          }
          disabled={disabled}
        />

        {helperText && helperText.length ? (
          <label className={_helperTextClassName}>{helperText}</label>
        ) : null}
      </div>
    );
  }

As we are using third party package react-select we have to style it according to its way. So I am passing the style object as a prop with _selectErrorStyle.

Now lets listen to different events and store the data, Also return it to the parent.

  //When single option will be selected
  handleOnChange = data => {
    data = data || {};
    const { name, onChange } = this.props;

    if (onChange) {
      onChange({ name, value: data });
    } else {
      this.setState({ value: data });
    }
  };
  
  //When multiple option will be selected
  handleMultiChange = value => {
    const { name, onChange } = this.props;

    if (onChange) {
      onChange({ name, value });
    } else {
      this.setState({ value: map(value) });
    }
  };

  //OnBlur event
  handleOnBlur = event => {
    const { name, onBlur } = this.props;
    onBlur && onBlur({ name, event });
  };

As this can be a controlled component, when we return the value on change to the parent, it may send us the updated value.

So we have to check if the received value is different then only update the component.

  componentDidUpdate(previousProps) {
    const { value } = this.props;

    //Update the state when recieved value is different
    if (!isEqual(previousProps.value, this.props.value)) {
      this.setState({ value });
    }
  }

Complete code of select component in react.

import React, { Component } from "react";
import PropTypes from "prop-types";
import ReactSelect from "react-select";
import cx from "classnames";
import { map, isEqual } from "lodash";
import styles from "./index.module.css";

class Select extends Component {
  static propTypes = {
    options: PropTypes.arrayOf(
      PropTypes.shape({
        value: PropTypes.any.isRequired,
        label: PropTypes.string.isRequired
      })
    ).isRequired,
    name: PropTypes.string.isRequired,
    /* Render multi dropdown */
    multi: PropTypes.bool,
    onChange: PropTypes.func,
    onBlur: PropTypes.func,
    prefillValue: PropTypes.any,
    label: PropTypes.node,
    className: PropTypes.string,
    labelClassName: PropTypes.string,
    value: PropTypes.any,
    helperText: PropTypes.string,
    innerRef: PropTypes.instanceOf(Element),
    error: PropTypes.bool,
    disabled: PropTypes.bool,
    disableOptionsByValue: PropTypes.arrayOf(PropTypes.any)
  };

  static defaultProps = {
    className: "",
    size: "default",
    disabled: false,
    disableOptionsByValue: []
  };

  constructor(props) {
    super(props);
    this.state = {
      value: props.value || props.prefillValue || ""
    };
  }

  componentDidUpdate(previousProps) {
    const { value } = this.props;

    //Update the state when recieved value is different
    if (!isEqual(previousProps.value, this.props.value)) {
      this.setState({ value });
    }
  }

  //When single option will be selected
  handleOnChange = data => {
    data = data || {};
    const { name, onChange } = this.props;

    if (onChange) {
      onChange({ name, value: data });
    } else {
      this.setState({
        value: data
      });
    }
  };

  //When multiple option will be selected
  handleMultiChange = value => {
    const { name, onChange } = this.props;

    if (onChange) {
      onChange({ name, value });
    } else {
      this.setState({
        value: map(value)
      });
    }
  };

  //OnBlur event
  handleOnBlur = event => {
    const { name, onBlur } = this.props;
    onBlur && onBlur({ name, event });
  };

  render() {
    const {
      className,
      multi,
      options,
      labelClassName,
      label,
      innerRef,
      error,
      helperText,
      disableOptionsByValue,
      disabled,
      ..._props
    } = this.props;

    let { value } = this.state;

    const _className = cx(className, styles.container, {
      [styles.hasError]: error
    });

    const _labelClassName = cx(labelClassName, styles.label, {
      [styles.error]: error
    });

    const _helperTextClassName = cx(styles.helperText, {
      [styles.error]: error
    });

    const _selectErrorStyle = error
      ? {
          control: (provided, state) => {
            const style = {
              ...provided,
              borderColor: "red",
              "&:hover": {
                borderColor: "red"
              }
            };

            if (state.isFocused) {
              return {
                ...style,
                boxShadow: "0 0 0 1px red"
              };
            } else {
              return {
                ...style
              };
            }
          },
          clearIndicator: provided => ({
            ...provided,
            color: "red"
          }),
          dropdownIndicator: provided => ({
            ...provided,
            color: "red"
          })
        }
      : {};

    return (
      <div className={_className}>
        {label ? <label className={_labelClassName}>{label}</label> : null}

        <ReactSelect
          {..._props}
          styles={_selectErrorStyle}
          options={options}
          isMulti={multi}
          onChange={multi ? this.handleMultiChange : this.handleOnChange}
          value={value}
          onBlur={this.handleOnBlur}
          ref={innerRef}
          isOptionDisabled={({ value }) =>
            disableOptionsByValue.includes(value)
          }
          disabled={disabled}
        />

        {helperText && helperText.length ? (
          <label className={_helperTextClassName}>{helperText}</label>
        ) : null}
      </div>
    );
  }
}

export default React.forwardRef((props, ref) => {
  return <Select {...props} innerRef={ref} />;
});

Complete CSS style of select component.

We don’t need to style the component much as we are already using a third party package.

//index.module.css
.container {
  display: inline-block;
  width: 100%;
  margin-bottom: 20px;
  padding: 0 10px;
}

.label {
  display: block;
  font-weight: 600;
  font-size: 14px;
  line-height: 24px;
  margin-bottom: 4px;
  letter-spacing: -0.05px;
  color: #4c4c56;
}

.hasError {
  border-color: #eb5055;
}

.hasError:focus {
  border-color: #eb5055;
}

.helperText {
  font-weight: 600;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 1px;
  display: block;
  position: absolute;
  bottom: 5px;
  color: #adb1b3;
  font-family: sans-serif;
}

.error {
  color: #eb5055;
}

Input

ReactDOM.render(
  <div className="abc">
    <Select
      name="abc"
      options={[
        { value: "car1", label: "Toyota" },
        { value: "car2", label: "Honda" },
        { value: "car3", label: "Hyundai" },
        { value: "car4", label: "BMW" }
      ]}
      label="Single Selection"
    />
    <Select
      name="abc"
      options={[
        { value: "car1", label: "Toyota" },
        { value: "car2", label: "Honda" },
        { value: "car3", label: "Hyundai" },
        { value: "car4", label: "BMW" }
      ]}
      multi={true}
      disableOptionsByValue={["car4"]}
      label="Mutliple Selection"
      value={[{ value: "car2", label: "Honda" }]}
    />
    <Select
      name="abc"
      options={[
        { value: "car1", label: "Toyota" },
        { value: "car2", label: "Honda" },
        { value: "car3", label: "Hyundai" },
        { value: "car4", label: "BMW" }
      ]}
      disabled={true}
      label="Disabled"
    />
    <Select
      name="abc"
      options={[
        { value: "car1", label: "Toyota" },
        { value: "car2", label: "Honda" },
        { value: "car3", label: "Hyundai" },
        { value: "car4", label: "BMW" }
      ]}
      error={true}
      label="Error"
    />
  </div>,
  document.getElementById("root")
);

Output

React Select