Create dropdown in react

Learn how to create dropdown in react.

Dropdown toggles contextual overlays or display box which can contain any thing.

We will also create something similar in react which will toggle an display panel on the click of the toggler.

To create this component we will use few extra packages.

  • prop-types: Used to validate the props we will receive.
  • classnames: This package helps us to use CSS classes as javascript objects, but to make it work we just need to name our CSS file as filename.module.css.

Following is the folder structure of our component.
React Dropdown folder structure

Let us begin creating our component by importing all the packages.

import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "./index.module.css";

class DropDown extends Component {
  //Other code will go here
}

export default DropDown;

We will be creating a stateful component by using a javascript class because,
1. We need to maintain the state of the component.
2. Need to use different lifecycle methods of react to handle certain events.

Now lets validate the props at the top before diving more into creating our component.

static propTypes = {
  isOpen: PropTypes.bool.isRequired,
  label: PropTypes.string.isRequired,
  children: PropTypes.node,
  onChange: PropTypes.func
};

static defaultProps = {
  isOpen: false,
  label: "DropDown",
  children: null
};

These are some of the generic props we are expecting. It will work as both controlled and uncontrolled component, I will show how to do it.

Define the state to store the changes within the component.

  state = {
    isOpen: this.props.isOpen
  };

After this create the layout of our component.

render() {
    const { label, children } = this.props;
    const { isOpen } = this.state;

    return (
      <div className={styles.wrapper}>
        <div
          className={styles.dropToggler}
          onClick={this.toggleDropDown}
          ref={ref => (this.dropTogglerRef = ref)}
        >
          <span className={styles.label}>{label}</span>
          <span className={styles.arrow}>{isOpen ? "\u25B2" : "\u25BC"}</span>
        </div>
        <div className={styles.displayArea}>
          {isOpen && (
            <div
              className={styles.children}
              ref={ref => (this.displayAreaRef = ref)}
            >
              {children}
            </div>
          )}
        </div>
      </div>
    );
  }

The layout is pretty simple, we are creating two different sections, one for the toggle button and one for the display area.

We are using Unicode characters to create the up and down icon.

We are also creating ref of these two sections ref={ref => (this.dropTogglerRef = ref)}, ref={ref => (this.displayAreaRef = ref)} because we will need it later.

The display area will only be rendered when the state isOpen is true.

Now lets define our click handler function which will change the state on the toggle button click.

  //DropDown toggler
  toggleDropDown = () => {
    const { onChange } = this.props;
    const { isOpen } = this.state;
    this.setState({
      isOpen: !isOpen
    });

    onChange && onChange(!isOpen);
  };

This will toggle the current state based on its previous state.

But we also want to close our dropdown if the click happens outside the display area. For these we will have to listen the click event of whole DOM and then update the state to close the dropdown accordingly.

To assign the event we will assign an event listener to the document. The best place to add this is in componentDidMount lifecycle method, because this way we will be sure that event is assigned only after the layout is generated.

componentDidMount() {
  //Assign click handler to listen the click to close the dropdown when clicked outside
  document.addEventListener("click", this.handleClickOutside);
}

Now lets create our function handleClickOutside to listen the event.

  //If click is outside the dropdown button or display area
  //Close the dropdown
  handleClickOutside = event => {
    const path = event.path || (event.composedPath && event.composedPath());
    const { onChange } = this.props;

    if (
      !path.includes(this.displayAreaRef) &&
      !path.includes(this.dropTogglerRef)
    ) {
      this.setState({
        isOpen: false
      });

      onChange && onChange(false);
    }
  };

As the toggle button click is also a click on the DOM and the same goes for the click on the display area. We can check if the path does not contain any of these with the refs that we had created in our layout.

Change the state to close only if either of them is not clicked.

While the components is getting removed we need to make sure that we remove this listener to avoid attaching multiple listener.

  componentWillUnmount() {
    //Remove the listener
    document.removeEventListener("click", this.handleClickOutside);
  }

This is an uncontrolled component, the isOpen props can only be used to set the initial state, after the component will maintain its own state.

Complete code of uncontrolled dropdown component.

import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "./index.module.css";

class DropDown extends Component {
  static propTypes = {
    isOpen: PropTypes.bool.isRequired,
    label: PropTypes.string.isRequired,
    children: PropTypes.node,
    onChange: PropTypes.func
  };

  static defaultProps = {
    isOpen: false,
    label: "DropDown",
    children: null
  };

  state = {
    isOpen: this.props.isOpen
  };

  componentDidMount() {
    //Assign click handler to listen the click to close the dropdown when clicked outside
    document.addEventListener("click", this.handleClickOutside);
  }

  componentWillUnmount() {
    //Remove the listener
    document.removeEventListener("click", this.handleClickOutside);
  }

  //If click is outside the dropdown button or display area
  //Close the dropdown
  handleClickOutside = event => {
    const path = event.path || (event.composedPath && event.composedPath());
    const { onChange } = this.props;

    if (
      !path.includes(this.displayAreaRef) &&
      !path.includes(this.dropTogglerRef)
    ) {
      this.setState({
        isOpen: false
      });

      onChange && onChange(false);
    }
  };

  //DropDown toggler
  toggleDropDown = () => {
    const { onChange } = this.props;
    const { isOpen } = this.state;
    this.setState({
      isOpen: !isOpen
    });

    onChange && onChange(!isOpen);
  };

  render() {
    const { label, children } = this.props;
    const { isOpen } = this.state;

    return (
      <div className={styles.wrapper}>
        <div
          className={styles.dropToggler}
          onClick={this.toggleDropDown}
          ref={ref => (this.dropTogglerRef = ref)}
        >
          <span className={styles.label}>{label}</span>
          <span className={styles.arrow}>{isOpen ? "\u25B2" : "\u25BC"}</span>
        </div>
        <div className={styles.displayArea}>
          {isOpen && (
            <div
              className={styles.children}
              ref={ref => (this.displayAreaRef = ref)}
            >
              {children}
            </div>
          )}
        </div>
      </div>
    );
  }
}

export default DropDown;

Now to create a controlled component we will have to use another lifecycle method called componentDidUpdate which gets called when the component updates.

Using this method we can check if the props that we have received and the state we are maintaining are same or not. If they are not same then update the state.

  //To control component
  componentDidUpdate() {
    if (this.props.isOpen !== this.state.isOpen) {
      this.setState({
        isOpen: this.props.isOpen
      });
    }
  }

Complete code of controlled dropdown component in react.

import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "./index.module.css";

class DropDown extends Component {
  static propTypes = {
    isOpen: PropTypes.bool.isRequired,
    label: PropTypes.string.isRequired,
    children: PropTypes.node,
    onChange: PropTypes.func
  };

  static defaultProps = {
    isOpen: false,
    label: "DropDown",
    children: null
  };

  state = {
    isOpen: this.props.isOpen
  };

  componentDidMount() {
    //Assign click handler to listen the click to close the dropdown when clicked outside
    document.addEventListener("click", this.handleClickOutside);
  }

  componentWillUnmount() {
    //Remove the listener
    document.removeEventListener("click", this.handleClickOutside);
  }

  //If click is outside the dropdown button or display area
  //Close the dropdown
  handleClickOutside = event => {
    const path = event.path || (event.composedPath && event.composedPath());
    const { onChange } = this.props;

    if (
      !path.includes(this.displayAreaRef) &&
      !path.includes(this.dropTogglerRef)
    ) {
      this.setState({
        isOpen: false
      });

      onChange && onChange(false);
    }
  };

  //DropDown toggler
  toggleDropDown = () => {
    const { onChange } = this.props;
    const { isOpen } = this.state;
    this.setState({
      isOpen: !isOpen
    });

    onChange && onChange(!isOpen);
  };

  //To control component
  componentDidUpdate() {
    if (this.props.isOpen !== this.state.isOpen) {
      this.setState({
        isOpen: this.props.isOpen
      });
    }
  }

  render() {
    const { label, children } = this.props;
    const { isOpen } = this.state;

    return (
      <div className={styles.wrapper}>
        <div
          className={styles.dropToggler}
          onClick={this.toggleDropDown}
          ref={ref => (this.dropTogglerRef = ref)}
        >
          <span className={styles.label}>{label}</span>
          <span className={styles.arrow}>{isOpen ? "\u25B2" : "\u25BC"}</span>
        </div>
        <div className={styles.displayArea}>
          {isOpen && (
            <div
              className={styles.children}
              ref={ref => (this.displayAreaRef = ref)}
            >
              {children}
            </div>
          )}
        </div>
      </div>
    );
  }
}

export default DropDown;

Styling our dropdown component in react.

Styling is also the major part of our component because we need to make sure that the display panel is always bounded to the toggle button.

So we will create a relative parent and make the panel absolute so that it is always inside it.

//index.module.css
.wrapper {
  display: inline-flex;
  position: relative;
  flex-direction: column;
}

.dropToggler {
  position: relative;
  padding: 10px;
  border: 1px solid #000;
  border-radius: 5px;
  font-size: 14px;
  color: #607d8b;
  cursor: pointer;
}

.dropToggler > .arrow {
  margin-left: 10px;
}

.displayArea {
  position: relative;
}

.displayArea > .children {
  position: absolute;
  left: 1px;
  top: 8px;
  min-width: 200px;
  min-height: 200px;
  background: #bdb8b8;
  box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
}

Input

Controlled Component

//test.js
import React, { Component } from "react";
import DropDown from "./index";

class DropDownTest extends Component {
  state = {
    isOpen: true
  };

  onChange = isOpen => {
    this.setState({
      isOpen
    });
  };

  render() {
    const { isOpen } = this.state;
    return <DropDown isOpen={isOpen} onChange={this.onChange} />
  }
}

export default DropDownTest;

Uncontrolled Component

import React, { Component } from "react";
import DropDown from "./index";

class DropDownTest extends Component {
  onChange = isOpen => {
    console.log(isOpen);
  };

  render() {
    return <DropDown isOpen={true} onChange={this.onChange} />
  }
}

export default DropDownTest;

Output

React dropdown