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.
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.
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)); }

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} />

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 — start monitoring for free.
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.
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).
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.
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.
Hi,
Can we have this example with react hooks/ Functional Component if possible
this is not working
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
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 😉
I got an error “toggleItem is not defined” when I passed the toggle item to the list item.
Please I need and urgent help.
One instance of incorrect code: isOpen should be isListOpen in the componentDidUpdate snippet!