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]);

Leave a Reply

Your email address will not be published. Required fields are marked *