Intro
In this post we’ll cover creating a Dropdown in React, and we’ll cover the following key touchpoints that you’ll come across day to day when building one:
- Open/Close the dropdown menu when button clicked
- Close the dropdown menu when clicking anywhere else on the page.
What we’ll be using to achieve this
- React Hooks { useState, useRef, useEffect }
- Css Modules
Prerequisite
A clean create-react-app project to use as the boilerplate. This also supports css modules out of the box.
- Npx create-react-app Dropdowns
Creating the component
Create a Dropdown component along with its css module as follows:
- src/components/Dropdown/Dropdown.js
- src/components/Dropdown/Dropdown.modules.css
In Dropdown.js, lets start by setting up our barebones functional component:
[ src/components/Dropdown/Dropdown.js]
import React from "react";
import * as style from "./Dropdown.module.css";
export default function Dropdown() {
return (
<div className={style.container}>
<button type="button" className={style.button}>
Click me!
</button>
</div>
);
}
As you can see, we simply have a button within a wrapping <div>
. We've imported the component's css, and have associated the wrapping <div>
with some 'container' styling, and the <button>
with some 'button' specific styling which we'll discuss.
Our css file thus far looks as follows:
src/components/Dropdown/Dropdown.module.css
.container {
position: relative;
display: inline-block;
}
.button {
padding: 0;
width: 100px;
border: 0;
background-color: #fff;
color: #333;
cursor: pointer;
outline: 0;
font-size: 20px;
}
Our wrapping <div>
has the ‘container’ style applied to it. Here we use ‘display: inline-block’ so that the width and hight will automatically adjust based in the size of the button.
Given we’re using the simply ‘Click me!’ text for our button, the ‘button’ styling here is purely for aesthetic purposes.
Adding the dropdown menu
Next up we need to add the dropdown menu to the component.
This is simply another <div>
with a <ul>
inside.
The important thing is that this dropdown menu is rendered as a child of the component’s wrapping <div>
container. This looks as follows:
[ src/components/Dropdown/Dropdown.js]
import React from "react";
import * as style from "./Dropdown.module.css";
export default function Dropdown() {
return (
<div className={style.container}>
<button type="button" className={style.button}>
Click me!
</button>
<div className={style.dropdown}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
</div>
</div>
);
}
The menu here is simply an unordered list, nothing fancy going on. The magic here is within the css module’s ‘dropdown’ definition, which we’ve associated with the menu’s wrapping <div>
using className={style.dropdown}.
The css is as follows:
[ src/components/Dropdown/Dropdown.module.css]
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
padding: 8px 12px;
}
li:hover {
background-color: rgba(0, 0, 0, 0.14);
cursor: pointer;
}
Status Check
So far, our running application should look as follows:
(assuming you’ve imported and dropped the component onto an empty page)
For now the dropdown menu is correctly positioned below our button.
Next we need to tackle opening and closing the menu – in other words hiding it until the button is clicked.
Clicking the <button>
to hide/unhide the menu
For this piece of functionality, we want to hide/unhide the menu when the button is clicked.
As you can imagine, we’ll need to the following to items to achieve this:
- An ‘onClick’ function to run when the button is clicked
- Some state to keep track of whether the button should be open or not.
As mentioned at the start of this post, we will be using React’s useState hook for managing our state, so we need to ensure we import it beside React.
Below are the changes to our component to achieve the hide/unhide functionality. We’ll discuss them below updated code.
[ src/components/Dropdown/Dropdown.js]
import React, { useState } from "react";
import * as style from "./Dropdown.module.css";
export default function Dropdown() {
const [dropdownState, setDropdownState] = useState({ open: false });
const handleDropdownClick = () =>
setDropdownState({ open: !dropdownState.open });
return (
<div className={style.container}>
<button
type="button"
className={style.button}
onClick={handleDropdownClick}
>
Click me!
</button>
{dropdownState.open && (
<div className={style.dropdown}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
</div>
)}
</div>
);
}
The first thing we’ve added above is state to keep track of whether the dropdown is open or not.
const [dropdownState, setDropdownState] = useState({ open: false });
Above, we say that the state is called ‘dropdownState’ and has a default value in the shape of an object; of which has an attribute called ‘open’ whose value is set to ‘false’ by default.
‘setDropdownState’ is simply the useState hook’s way of letting us alter the value of the ‘dropdownState’ whenever we need to.
Next up, we need to create a function to fire whenever our button is clicked.
const handleDropdownClick = () =>
setDropdownState({ open: !dropdownState.open });
Above, we’ve created a simply ‘handleDropdownClick’ function, whose sole purpose is to update the value of our ‘dropdownState’ - initially from the default value of {open: false} to {open: true} and subsequently to simply flip the Boolean value, which is achieved with the ‘!’.
Next up, you can see we’ve added the onClick event to our button:
<button
type="button"
className={style.button}
onClick={handleDropdownClick}
>
Click me!
</button>
And finally, we needed to tell our dropdown menu (a.k.a the unordered list), to only render if the ‘dropdownState’ is set to {open: true}.
We have achieved this as follows:
{dropdownState.open && (
<div className={style.dropdown}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
</div>
)}
This means that if dropdownState.open is truthy, continue to render the menu list, implicitly implying that if it’s falsey, it will not.
Status Check
At this point, our application should now be fully functioning.
On initial load, you button should render as follows:
dropdownState = {open: false}
And when the button is clicked, the menu should render as follows:
dropdownState = {open: true}
And when the button is clicked again, the menu should hide as follows:
dropdownState = {open: false}
The missing piece to the dropdown jigsaw - Outside Clicks?
One final caveat you may or may not have noticed, is that although our menu successfully opens and closes upon clicking our button, it does not close if you click anywhere else on the page.
The final piece we need to achieve is to ensure the menu closes when the user clicks on any part of the document that is not our component.
I specifically say ‘not our component’ because we don’t want the menu to close if the user clicks a menu item, as that would be a bad user experience. And we don’t have to worry about the button itself, because we already have an ‘onClick’ function handling this.
For this final piece of functionality (the outside clicking), we will be using the following features:
- React’s useRef hook
- React’s useEffect hook
- Event Listeners
Adding a Ref
Firstly, we need to make sure we import the useRef and useEffect hook at the top of our file:
import React, { useState, useRef, useEffect } from "react";
Then we need to create a Ref using the useRef hook and assign it to our outermost <div>
.
The <div>
we’re assigning to the Ref to is the <div>
with the container styling, which is also essentially the parent container of our component. So lets call the ref ‘container as below:
const container = useRef();
And then we assign it to the outermost <div>
as such:
<div className={style.container} ref={container}>
What is this ref for?
We need the useRef hook to essentially get access to the DOM. So by assigning our wrapping <div>
a ref, it means we can access the div’s RAW DOM node using the ref’s .current property.
This may become clearer when we actually use this next.
Outside Clicks
Next up we need to add EventListeners to the document (the page) in order to listen for user clicks (which we’ll use the in-built “mousedown” event for).
One key thing to note about adding EventListeners to the document, is that it’s always best practise to clean them up when done too.
For our EventListeners, we’ll be adding them to the document within the useEffect hook – which will essentially give us the equivalent functionality of componentDidMount() and componentWillUnmount().
Per the below code, we add the “mousedown” (a.k.a the click) EventListener within the useEffect – and by the very nature of the useEffect hook it means the listener will be added to the document (the page) when our Dropdown component mounts. This is efficient in the sense that we don’t need the EventListener until our component has mounted (is on the page).
Then, you can see we remove the EventListener in useEffect’s return. Why? This is again by nature of the useEffect hook. If you return a function within useEffect, it runs that function when the component unmounts (when it’s removed from the page).
// attaches an eventListener to listen when componentDidMount
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
// optionally returning a func in useEffect runs like componentWillUnmount to cleanup
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
Finally, we need to define our handleClickOutside function, which is what occurs above when our “mousedown” EventListener is fired once someone clicks on the page.
In our handleClickOutside function, we need to check and ensure that our ‘current’ is actually filled in with a DOM element. We do this with checking if ‘container.current’ is truthy, and if it is, we then check if we the event target of the DOM node that was clicked. (using ‘!container.current.contains(e.target)’)
If we do not have the clicked target, it means the click has occurred outside of our ‘container’ – so we close our menu by updating our dropdownState.
const handleClickOutside = (e) => {
if (container.current && !container.current.contains(e.target)) {
setDropdownState({ open: false });
}
};
Given that our ‘button’ is inside of our ‘container’, it still runs it’s onClick function as normal, so is unaffected by our additional code.
Finally, our finished component should look as follows:
[src/components/Dropdown/Dropdown.js]
import React, { useState, useRef, useEffect } from "react";
import * as style from "./Dropdown.module.css";
export default function Dropdown() {
const container = useRef();
const [dropdownState, setDropdownState] = useState({ open: false });
const handleDropdownClick = () =>
setDropdownState({ open: !dropdownState.open });
const handleClickOutside = (e) => {
if (container.current && !container.current.contains(e.target)) {
setDropdownState({ open: false });
}
};
// attaches an eventListener to listen when componentDidMount
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
// optionally returning a func in useEffect runs like componentWillUnmount to cleanup
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className={style.container} ref={container}>
<button
type="button"
className={style.button}
onClick={handleDropdownClick}
>
Click me!
</button>
{dropdownState.open && (
<div className={style.dropdown}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
</div>
)}
</div>
);
}
Top comments (1)
Thank you! Very useful