DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Building a custom dropdown menu component for React

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 might be in your best interest, considering the time that might be spent 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.

Visual structure

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

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>
  <ul className="dd-list">
    <li className="dd-list-item"></li>
    <li className="dd-list-item"></li>
    <li className="dd-list-item"></li>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode
  • We need to be able to toggle the dd-list upon clicking on dd-header and close it when clicked outside the dd-wrapper
  • We need to populate the <li> tags automatically based on data
  • We need to be able to control the header title

Before we can start to meet these requirements, we must decide whether to use functional component or class component.

Functional component or class component?

Functional components became faster in the latest release of React 16.

However, it is not always possible to take advantage when you need the state definition either in the component or any of the component lifecycle hooks.

For this specific example, it is possible to implement without a state definition or lifecycle hooks, but deploying them makes things more tidy and straightforward.

Using a functional component would require passing some variables as props. When we interact with the dropdown menu, we would be changing these props. Changing parent component’s props from a child component requires passing functions from parent to child as props so that you can control the parent component’s state.

If you overdo it, things will get complicated quickly. So, there are always trade-offs that should be considered.

We are going to deploy a class component with state and lifecycle hooks, whereas we will also be using functions as props in order to control the parent state.

Component relations

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'
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Key variable comes in very 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 and shape the component accordingly. Below you see the Dropdown component used in a parent component.

<Dropdown
  title="Select location"
  list={this.state.location}
/>
Enter fullscreen mode Exit fullscreen mode

We have a title to show and an array of data.

Before editing the render() method, we need a component state definition.

constructor(props){
  super(props)
  this.state = {
    listOpen: false,
    headerTitle: this.props.title
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Below see the render() method for the current configuration along with toggleList() and handleClickOutside() methods for toggling the list and closing the list when clicked outside the dd-wrapper respectively.

Note that handleClickOutside() comes from a third party HOC (Higher order component) named react-onclickoutside .

Moreover, FontAwesome is a component wrapping the font-awesome icon library.

handleClickOutside(){
  this.setState({
    listOpen: false
  })
}
toggleList(){
  this.setState(prevState => ({
    listOpen: !prevState.listOpen
  }))
}
render(){
  const{list} = this.props
  const{listOpen, headerTitle} = this.state
  return(
    <div className="dd-wrapper">
    <div className="dd-header" onClick={() => this.toggleList()}>
        <div className="dd-header-title">{headerTitle}</div>
        {listOpen
          ? <FontAwesome name="angle-up" size="2x"/>
          : <FontAwesome name="angle-down" size="2x"/>
        }
    </div>
     {listOpen && <ul className="dd-list">
       {list.map((item) => (
         <li className="dd-list-item" key={item.id} >{item.title}</li>
        ))}
      </ul>}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

With a styling applied, we get the following results.

Notice that we also deployed listOpen for toggling the arrow icon up or down by using the conditional (ternary) operator.

Dropdown menu, closed and open

Controlling a parent state from a child

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.

Passing a function, which uses setState , as a prop enables you to control the other props.

What you are doing is basically calling a function, which is defined in the parent component, from your child component to trigger the setState , which changes the state that passed as a prop in the first place.

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

Control function in the parent

Defining the following function in the parent component and passing it as a prop to a child component, which is the Dropdown component, would enable us to control the desired information in the desired state.

toggleSelected(id, key){
  let temp = this.state[key]
  temp[id].selected = !temp[id].selected
  this.setState({
    [key]: temp
  })
}
Enter fullscreen mode Exit fullscreen mode

Notice that we have id and key parameters for the toggleSelected() function.

Remember that we defined a key/value pair named key for each object inside the location array, now it is time to utilize it.

By using the key, we can tell toggleSelected() function which state variable to change.

key = "location"
//These two refers to the same state variable
- this.state.location
- this.state[key]
Enter fullscreen mode Exit fullscreen mode

Likewise, the id tells which object to refer to in the location array variable.

Time to pass it as a prop:

<Dropdown
  title="Select location"
  list={this.state.location}
  toggleItem={this.toggleSelected}
/>
Enter fullscreen mode Exit fullscreen mode

Call it inside the <li> tag:

<li className="dd-list-item" key={item.title} onClick={() => toggleItem(item.id, item.key)}>{item.title} {item.selected && <FontAwesome name="check"/>}</li>
Enter fullscreen mode Exit fullscreen mode

Also, notice that I have added an icon, depending on the value of item.selected, to indicate that the item is selected.

Multiple items selected in the list

Dynamic header title

One last thing I wanted to have was a dynamic header title which changes in accordance with the number of selected items.

All we need to do is count how many of the selected key/value pairs are true and then change the headerTitle state in the Dropdown component accordingly.

We should update the component state when the location prop changes. In order to do so, we need to listen to prop updates through a life-cycle hook.

static getDerivedStateFromProps() is what we need.

It is a new lifecycle hook replacing the old componentWillReceiveProps() method.

Since it is a static method, it has no access to this . That would deprive us of using this.setState and this.props .

With this new method, you either return null to indicate that there is no change or you directly return the state modification. Also, since you have no access to this.props due to static method, you should store the previous props in a state and then reach them through prevState .

static getDerivedStateFromProps(nextProps, prevState)

We need to pass one more prop to Dropdown component to use for controlling the header title.

<Dropdown
  titleHelper="Location"
  title="Select location"
  list={this.state.location}
  toggleItem={this.toggleSelected}
/>
Enter fullscreen mode Exit fullscreen mode

static getDerivedStateFromProps(nextProps){
    const count = nextProps.list.filter(function(a) { return a.selected; }).length;
    console.log(count)
if(count === 0){
      return {headerTitle: nextProps.title}
    }
    else if(count === 1){
      return {headerTitle: `${count} ${nextProps.titleHelper}`}
    }
    else if(count > 1){
      return {headerTitle: `${count} ${nextProps.titleHelper}s`}
    }
  }
Enter fullscreen mode Exit fullscreen mode

The header title changes in accordance with the number of selected items in the list is shown below:

Dynamic header title

Conclusion

With my approach towards implementing a dropdown menu component for one of my projects, I basically kept the dropdown menu content data in the wrapping parent component and passed it as a prop. Passing a function also as a prop was the trick to control the parent state, which was eventually the data used in the Dropdown component.

Using static getDerivedStateFromProps() helped controlling the header title based on interaction with the dropdown menu.

Plug: LogRocket, a DVR for web apps

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


Top comments (0)