Doğacan Bilgili A software developer who is also into 3D Modeling/Animation.

Build a custom dropdown menu component for React

7 min read 2088

React custom dropdown menu component

It’s true that adapting an existing component into your project might not always go as smoothly as you’d like when it comes to specific requirements and styling. In such cases, building your own component may be in your best interest, considering the time you can spend on the adaptation process.

This article will walk you through an approach that I followed in my personal project to create a custom dropdown menu component in React.

For the complete source code and the styling files you can refer to the GitHub repo.

The visual structure of a dropdown menu component

Before diving into the technical stuff, let’s quickly look at the visual structure of the dropdown menu component and decide on the requirements.

Custom dropdown menu for React
Visual structure of a dropdown component.

A dropdown menu consists of four basic components:

  • header wrapping
  • header title
  • list wrapping
  • list items

The corresponding HTML could look like this:

  
<div className="dd-wrapper">
  <div className="dd-header">
    <div className="dd-header-title"></div>
  </div>
  <div className="dd-list">
    <button className="dd-list-item"></button>
    <button className="dd-list-item"></button>
    <button className="dd-list-item"></button>
  </div>
</div>
  • We need to be able to toggle the dd-list upon clicking dd-header and close it when clicked outside the dd-wrapper
  • We need to populate the <button> tags automatically based on data
  • We need to be able to control the header title dynamically

Parent-child relations in components

A parent component holds single or multiple dropdown menus and since each dropdown menu has a unique content, we need to parameterize it by passing information as props.

Let’s assume we have a dropdown menu, where we select multiple locations.

Consider the following state variable inside the parent component:

constructor(){
  super()
  this.state = {
    location: [
      {
          id: 0,
          title: 'New York',
          selected: false,
          key: 'location'
      },
      {
          id: 1,
          title: 'Dublin',
          selected: false,
          key: 'location'
      },
      {
          id: 2,
          title: 'California',
          selected: false,
          key: 'location'
      },
      {
          id: 3,
          title: 'Istanbul',
          selected: false,
          key: 'location'
      },
      {
          id: 4,
          title: 'Izmir',
          selected: false,
          key: 'location'
      },
      {
          id: 5,
          title: 'Oslo',
          selected: false,
          key: 'location'
      }
    ]
   }
  }

We have a unique id to use with key prop of map method when populating the location array, a title for each item in the list, a boolean variable named selected in order to toggle the selected items in the list (in case of multiple selections in a dropdown menu), and a key variable.

We made a custom demo for .
No really. Click here to check it out.

key variable comes in handy for using with setState function. I will touch on that later.

Now let’s take a look at what we passed to Dropdown component as props so far. Below you see the Dropdown component used in a parent component, where we have passed a title to show and an array of data to populate the dropdown list.

<Dropdown  
  title="Select location"
  list={this.state.location}
/>

Before editing the render() method, we need the following state variables in our dropdown component:

<constructor(props){
  super(props)
  this.state = {
    isListOpen: false,
    headerTitle: this.props.title
  }
}

Here we have a isListOpen boolean variable for toggling the menu list and a headerTitle, which is equal to the title prop by default.

Now, take a look at the render() method of our component. Note that the <FontAwesome> used in the render JSX markup is an external NPM package and should installed and imported inside the dropdown component.

import FontAwesome from 'react-fontawesome';

On top of that, you also need to include the following <link> tag in the index.html of your project. This is required for FontAwesome to work properly.

<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />

Here in the render method. We have the aforementioned structure with the header and the list containing the list items. You will notice the toggleList() and selectItem() functions used in the render method, so let’s create them.

render() {
  const { isListOpen, headerTitle } = this.state;
  const { list } = this.props;

  return (
    <div className="dd-wrapper">
      <button
        type="button"
        className="dd-header"
        onClick={this.toggleList}
      >
        <div className="dd-header-title">{headerTitle}</div>
        {isListOpen
          ? <FontAwesome name="angle-up" size="2x" />
          : <FontAwesome name="angle-down" size="2x" />}
      </button>
      {isListOpen && (
        <div
          role="list"
          className="dd-list"
        >
          {list.map((item) => (
            <button
              type="button"
              className="dd-list-item"
              key={item.id}
              onClick={() => this.selectItem(item)}
            >
              {item.title}
              {' '}
              {item.selected && <FontAwesome name="check" />}
            </button>
          ))}
        </div>
      )}
    </div>
  )
}

What toggleList() function does is simply toggle the isListOpen state variable and thus showing or hiding the items list.

toggleList = () => {
   this.setState(prevState => ({
     isListOpen: !prevState.isListOpen
  }))
}

selectItem() function, on the other hand, sets the headerTitle state to selected item’s title and sets isListOpen state to false to close the list upon selection.

After setting these states, it calls the resetThenSet() callback function, which is a prop we need to pass to Dropdown /.

This will be explained in the next chapter.

Calling this callback function updates the location state in the parent component and marks the clicked list item as selected.

selectItem = (item) => {
  const { resetThenSet } = this.props;
  const { title, id, key } = item;

  this.setState({
    headerTitle: title,
    isListOpen: false,
  }, () => resetThenSet(id, key));
}
custom dropdown menu open close
Dropdown menu, closed and open.

Controlling a parent state from a child component

When you pass something as a prop to a child component, you can only use that data and cannot change it unless you deploy additional props. If you define a function in the parent component, which controls the state, and then pass this function as a prop to child component, then you can call this function from the child component and set the parent component’s state.

In the case of the dropdown, when a list element is clicked, we need to be able to toggle the selected key for the corresponding object in the location state of the parent component.

We do this with the resetThenSet() function passed as a prop to dropdown component.

This function clones the location state, then sets the selected key of each object in the array to false and then only sets the clicked item’s selected key to true, hence the name resetThenSet.

This function is defined in the parent component:

resetThenSet = (id, key) => {
  const temp = [...this.state[key]];

  temp.forEach((item) => item.selected = false);
  temp[id].selected = true;

  this.setState({
    [key]: temp,
  });
}

And then passed to <Dropdown /> component as a prop:

<Dropdown
  title="Select location"
  list={this.state.location}
  resetThenSet={this.resetThenSet}
/>

Single- or multi-select dropdown

This setup was required for single-select dropdown. However, if we want to be able to select multiple items in the dropdown, then we need a different function in place of resetThenSet().

We name that function toggleItem() because it only toggles the selected key of the items in the location array.

toggleItem = (id, key) => {
  const temp = [...this.state[key]];

  temp[id].selected = !temp[id].selected;

  this.setState({
    [key]: temp,
  });
}

Then we need to pass this function as a prop as we did before:

<Dropdown
  title="Select location"
  list={this.state.location}
  toggleItem={this.toggleItem}
/>

And when using it in the <Dropdown/> component, we can directly call it without an intermediate function, unlike what we did before, given that we don’t need to set the headerTitle or close the list.

However, we still need to handle the headerTitle so that we can show how many locations are selected.

render() {
  const { list, toggleItem } = this.props;

  return (
    //
    //
      <button
        type="button"
        className="dd-list-item"
        key={item.id}
        onClick={() => toggleItem(item.id, item.key)}
      >
    //
    //
  )
}

Dynamic header title

As mentioned in previously, we didn’t set the headerTitle in case of multi-select dropdown. However, no matter it’s a single- or multi-select dropdown, we need to handle the headerTitle separately due to the fact that the list array passed might contain items with selected key set to true by default. The component should be able to detect this and set the headerTitle accordingly.

In order to handle this, we are going to use the static getDerivedStateFromProps lifecycle hook.

The purpose of getDerivedStateFromProps is to enable a component to update its internal state as a result of changes in props. It should return an object to update the state, or return null if nothing needs to be updated.

Single-select dropdown menu

First, filter the list prop to see if there is any object with the selected key set to true. If there is any, that will be returned and will be available in selectedItem. Then we use this object’s title key to set the headerTitle. If selectedItem is empty, then we simply return and object where we set title prop to headerTitle.

static getDerivedStateFromProps(nextProps) {
  const { list, title } = nextProps;
  const selectedItem = list.filter((item) => item.selected);

  if (selectedItem.length) {
    return {
      headerTitle: selectedItem[0].title,
    };
  }
  return { headerTitle: title };
}

Multi-select dropdown menu

When dealing with multi-selection, we check the length of the items with selected key set to true. If this count is equal to 0, then we simply set headerTitle to default title prop.

If the count is equal to 1 then we use a prop called titleHelper. In our case this is a string value equal to "Location" in order to be able to display 1 location on the title.

If the count is greater than 1, we use the plural form of location, which we provide to our component through titleHelperPlural prop. This props is equal to "Locations" in our case.

static getDerivedStateFromProps(nextProps) {
  const {
    list,
    title,
    titleHelper,
    titleHelperPlural
  } = nextProps;

  const count = list.filter((item) => item.selected).length;

  if (count === 0) {
    return { headerTitle: title };
  }
  if (count === 1) {
    return { headerTitle: `${count} ${titleHelper}` };
  }
  if (count > 1) {
    return { headerTitle: `${count} ${titleHelperPlural}` };
  }
  return null;
}

So, our component will have the following props if it’s a multi-select dropdown:

<Dropdown
  titleHelper="Location"
  titleHelperPlural="Locations"
  title="Select location"
  list={this.state.location}
  toggleItem={this.toggleItem}
/>
custom dropdown menu Dynamic header title
Dynamic header title.

Handling outside clicks

The last thing we need to handle is to close the dropdown when clicked outside of it.

It is quite straightforward to listen to click events on window object and toggle the isListOpen state variable. However, this approach requires some small tricks to make it work properly.

Consider the following snippet where we add an event listener to window object depending on isListOpen state variable. However, this attempt results in a tooltip opening and closing virtually simultaneously.

close = () => {
  this.setState({
    isListOpen: false,
  });
}

componentDidUpdate(){
  const { isListOpen } = this.state;

  if(isOpen){
    window.addEventListener('click', this.close)
  }
  else{
    window.removeEventListener('click', this.close)
  }
}

The solution is to use setTimeout method with 0 milliseconds delay or without any time delay defined so that it queues a new task to be executed by the next event loop. Although using 0 milliseconds usually describes a task that should be executed immediately, this is not the case with the single-thread synchronous nature of JavaScript.

When the setTimeout is used, it simply creates an asynchronous callback. You can refer to the specific MDN web docs for a detailed explanation on the topic.

componentDidUpdate(){
  const { isListOpen } = this.state;

  setTimeout(() => {
    if(isListOpen){
      window.addEventListener('click', this.close)
    }
    else{
      window.removeEventListener('click', this.close)
    }
  }, 0)
}

There is one more thing we need to take into account. When using the dropdown in multi-select mode, it is likely that we don’t want to close the list when an item selected unlike single select mode. To fix this issue, we need to call stopPropagation() method on onClick event of the list items.

This prevents propagation of the same event bubbling up to parent elements and thus keeps the item list open when items are being clicked.

<button
  type="button"
  className="dd-list-item"
  key={item.id}
  onClick={(e) => {
    e.stopPropagation();
    this.selectItem(item);
  }}
>

Conclusion

In this tutorial, we structured a dropdown menu component that supports both single- and multi-select functionalities. We learned how to control the parent component’s state from a child component by passing functions as props to child component and calling them inside the child component.

Moreover, we used static getDerivedStateFromProps method in order to update the state variables upon prop changes.

Note that this tutorial just gives an introductory approach on how to create a custom dropdown menu. In order to create a fully fledged dropdown component, you need to bear accessibility in mind as well.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Doğacan Bilgili A software developer who is also into 3D Modeling/Animation.

9 Replies to “Build a custom dropdown menu component for React”

  1. The code snippets are not accurate. I tried following along but your syntax errors were too confusing to try to fix. Look at the portion where you introduce FontAwesome for random angle brackets and closing divs with no opens.

    1. Hi, Alex. Should be all set now. This isn’t Doğacan’s mistake. The formatting got borked when we migrated the blog to wordpress. Thanks for pointing it out (and sorry for the hassle).

  2. This tutorial is not bad (you can actually use functions with hooks and avoid using classes), but without the styles for the components I can’t really see the example that you produce here. So I would add a codepen or something with the whole component.

  3. Hey Dani,
    That’s true that now you can do that with hooks, but that was not an option back then when this tutorial was written.
    As for the styling, you can actually find the complete source code in the GitHub repo which I linked at the very beginning of this article.

  4. Im not sure how useful this tutorial is as I did not try it 🙂 But just by looking the code, I saw that it is not working as it should. A custom select should be accessible through a keyboard, like html select is, for accessibility reasons and for you to be able to select an option from your keyboard.
    So this tutorial has these things wrong:
    – No `role=”option”` and `role=”listbox”` attributes were set
    – There are no handlers for simulating a default’s “select” behavior, like navigating with the arrows.
    – It’s inaccessible

  5. I was about to start reading the tutorial, when I come across your review I changed my mind. thank you Mr. You are a time saver 😉

  6. I got an error “toggleItem is not defined” when I passed the toggle item to the list item.
    Please I need and urgent help.

Leave a Reply