Creating a Custom Dropdown Menu Component in React
When it comes to adding styling and specific requirements to your application, adapting an existing component may not always be the best approach. Building your own custom component can be more efficient and effective in the long run.
The Visual Structure of a Dropdown Menu Component
Before diving into the technical aspects, let’s break down the visual structure of a dropdown menu component:
- Header wrapping
- Header title
- List wrapping
- List items
The corresponding HTML code would look like this:
“`html
- Item 1
- Item 2
- Item 3
“`
We need to be able to toggle the dd-list
upon clicking the dd-header
and close it when the user clicks outside the dd-wrapper
.
Parent-Child Relations in Dropdown Components
A parent component holds one or multiple dropdown menus. Since each dropdown menu has unique content, we need to parameterize it by passing information as props.
Let’s imagine we have a dropdown menu where we can select multiple locations. We’ll pass a state variable locations
as a prop to the Dropdown
component:
javascript
const [locations, setLocations] = useState([
{ id: 1, title: "Location 1", selected: false },
{ id: 2, title: "Location 2", selected: false },
{ id: 3, title: "Location 3", selected: false },
]);
We’ll then pass the locations
array and a title
prop to the Dropdown
component:
javascript
<Dropdown title="Select Location" data={locations} />
Controlling a Parent State from a Child Component
To control the parent component’s state from a child component, we can pass functions as props to the child component and call them inside the child component.
In our case, we’ll pass a resetThenSet
function as a prop to the Dropdown
component:
javascript
const resetThenSet = (id) => {
setLocations((prevLocations) =>
prevLocations.map((location) =>
location.id === id ? { ...location, selected: true } : { ...location, selected: false }
)
);
};
We’ll then call the resetThenSet
function inside the Dropdown
component:
javascript
const handleItemClick = (id) => {
resetThenSet(id);
};
Single or Multi-Select Dropdown
Our setup so far is for a single-select dropdown. However, if we want to create a multi-select dropdown, we need to modify our approach.
We’ll create a new function toggleItem
that toggles the selected
key of the items in the locations
array:
javascript
const toggleItem = (id) => {
setLocations((prevLocations) =>
prevLocations.map((location) =>
location.id === id ? { ...location, selected: !location.selected } : location
)
);
};
We’ll then pass the toggleItem
function as a prop to the Dropdown
component:
javascript
<Dropdown title="Select Location" data={locations} toggleItem={toggleItem} />
Dynamic Header Title
We need to handle the header title separately to show how many locations are selected.
We’ll use the static getDerivedStateFromProps
method to update the state variables upon prop changes:
javascript
static getDerivedStateFromProps(props) {
const selectedItem = props.data.find((item) => item.selected);
if (selectedItem) {
return { headerTitle: selectedItem.title };
}
return null;
}
For a multi-select dropdown, we’ll check the length of the items with the selected
key set to true
:
javascript
static getDerivedStateFromProps(props) {
const count = props.data.filter((item) => item.selected).length;
if (count > 0) {
return { headerTitle: `${count} ${props.titleHelperPlural}` };
}
return null;
}
Handling Outside Clicks
Finally, we need to handle closing the dropdown menu when a user clicks outside of it.
We’ll add an event listener to the window object that depends on the isListOpen
state variable:
javascript
useEffect(() => {
const handleClickOutside = () => {
if (isListOpen) {
setIsListOpen(false);
}
};
window.addEventListener("click", handleClickOutside);
return () => {
window.removeEventListener("click", handleClickOutside);
};
}, [isListOpen]);
However, this approach requires some small tricks to make it work properly. We’ll use the setTimeout
method with a 0 millisecond delay to queue a new task to be executed by the next event loop:
javascript
useEffect(() => {
const handleClickOutside = () => {
setTimeout(() => {
if (isListOpen) {
setIsListOpen(false);
}
}, 0);
};
window.addEventListener("click", handleClickOutside);
return () => {
window.removeEventListener("click", handleClickOutside);
};
}, [isListOpen]);