Building an Recursive Nested Dropdown Component in React

Michael Chang
Tripping Engineering
6 min readJun 23, 2017

--

While scoping out a project to rebuild the Tripping.com homepage, I encountered a seemingly simple nested dropdown, which needed to be recreated in React. The “Explore” dropdown included Top Locations, Major Cities, Rental Types, and Content Center (our blog) — each with its own submenu of links to other parts of our site. To build this nested dropdown, it would have been relatively simple to nest unordered lists twice, iterating through options provided.

However, in our React framework, we aim to build our components in an abstract way whenever possible for three main reasons:

  1. Reusability: This allows us to develop more quickly since already-built components can be re-used in another part of the site without the need to completely rebuild a similar, but slightly variant, component.
  2. Flexibility: We also want to keep in mind the ability to deal with variations in these components, so that this single component can manage variations in styling or usage (including number of nested levels, font-size, use of icon, etc.).
  3. Manageability: As a general rule, we want to keep our codebase lean, which makes it a lot more manageable. The fewer components we have to test, the easier it is to maintain a relatively bug-free site!

With these goals in mind, I wanted to anticipate any potential future use cases. That way, if we ever need a Nested Dropdown that required more than two levels of nested submenus, we could still use this same abstracted component with minimal customizations.

Determining the PropTypes

First, the component itself doesn’t need to know how deep it will be nested. We simply pass in the dropdown options as props into our component, based on the expected propTypes:

const shape = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
link: PropTypes.string,
options: PropTypes.arrayOf(Prop.Type.shape(NestedDropdown.shape))
};
static propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape(NestedDropdown.shape).isRequired
).isRequired
};

If any of the dropdown options has a nested submenu, it will contain its own options property, mirroring the same shape of id, text, link, and (if nested another level)options.

Using Recursion to Build Our Component

Because of the unknown number of nested options, I opted to use recursion to render the Nested Dropdown. First, the renderSubMenu function takes in options and iterates through each option, returning an unordered list of list items.

renderSubMenu(options) {
const menuOptions = options.map(option => {
const display = (option.link
? <a href={ option.link }>{ option.text }</a>
: <span>{ option.text }</span>
);
return (
<li key={ option.id }>
{ display }
</li>
);
};
return (
<ul>
{ menuOptions }
</ul>
)
}

If that current option has a nested submenu, I then recursively call the renderSubMenu function with its own options props. This continues until the base case occurs — whenever the option being rendered no longer has its own nested submenu.

renderSubMenu(options) {
const menuOptions = options.map(option => {
...
let subMenu; if (option.options && option.options.length > 0) {
subMenu = this.renderSubMenu(option.options)
}
return (
<li>
{ display }
{ subMenu }
</li>
);
};
...
}

Theoretically, this recursive function can be called an infinite number of times, although any real-world use case would likely be capped at a maximum of three or four levels.

Rendering Only Active Menu Option and its Submenus

Given that a nested dropdown of even three or four levels would result in quite a high number of HTML tags on the document, I wanted to minimize the extra nodes by rendering only the submenus that were “active” — that is, only when the user was currently hovering over that option.

To achieve this, I utilized state in React. By setting an array of selectedIds in the state, I could efficiently keep track of the current menu and submenu(s) that were “active”.

export default class NestedDropdown extends React.Component {  static displayName = 'NestedDropdown';

constructor(props) {
super(props);
this.state = {
selectedIds: []
};
};
...};

Anytime a user hovers over a dropdown option, the event handler handleSelectedId will add the current option id into the array selectedIds at the corresponding array index (depthLevel).

For example, a user hovering over a submenu 3 levels deep would have 3 id’s pushed into the array, the third id stored at depthLevel (or array index) of 3.

handleSelectedId = (selected, depthLevel) => { 
return () => {
const updatedArray = this.state.selectedIds.slice(0);
updatedArray[depthLevel] = selected; this.setState({
selectedIds: updatedArray
});
};
};

Let’s circle back to our recursive function renderSubMenu in order to properly trigger our event handler, handleSelectedId. When we render each option, we attach an onMouseEnter event listener to each option that then calls the handleSelectedId upon hover, where we pass in the current option id and depthLevel, incremented each time renderSubMenu is recursively called. That way, each option is associated to the proper level it is nested within the entire dropdown.

renderSubMenu(options, depthLevel = 0) {
const menuOptions = options.map(option => {
const display = (option.link
? <a href={ option.link }>{ option.text }</a>
: <span>{ option.text }</span>
),
hasOptions = (option.options
&& option.options.length > 0
);
let subMenu; // only render selected submenu and only if nested options exist
if ((this.state.selectedIds[depthLevel] === option.id)
&& hasOptions
) {
const newDepthLevel = depthLevel + 1;
subMenu = this.renderSubMenu(option.options, newDepthLevel);
}
return (
<li
key={ option.id }
onMouseEnter={
this.handleSelectedIndex(option.id, depthLevel)
}
>
{ display }
{ subMenu }
</li>
);
});
return (
<div className=‘dropdown__options'>
<ul>
{ menuOptions }
</ul>
</div>
);
}

We only recursively call the renderSubMenu function if and only if the option has a nested submenu and the selectedId at the current depthLevel matches the current id.

Allowing for Flexibility in Styling & Usage

In addition for reusability, we also want to develop with the flexibility necessary to accommodate variations in usage and styling. For example, the Nested Dropdown might have the option to show an icon next to the text. Or it might open to the left or right. Or perhaps, the font-size might change depending on which part of the site it resides.

In this particular use case, we want to allow for the presence or absence of an icon in the display — a caret that points down.

To allow this variation, we simply pass in a prop hasCaret with the propType of boolean. This allows us to utilize Nested Dropdown here with a caret, but also to reuse in a different part of the site where we might not actually want a caret icon.

renderDisplay() {
const classes = classNames({
'dropdown__display': true,
'dropdown__display--with-caret': this.props.hasCaret
}),
caret = (
<Icon
classes={ ['dropdown__display-caret'] }
glyph={ iconChevronDown }
size={ 'small' }
/>
);
return (
<div className={ classes }>
{ this.props.displayText }
{ this.props.hasCaret ? caret : null }
</div>
);
}

*<Icon /> is another reusable component used throughout our app.

Lastly, we want to allow for flexibility in the direction that the menu opens. To achieve this, we again pass in a prop — openDirection, either left or right. We then use the prop to set variable classNames onto the submenus.

renderSubMenu(options, depthLevel = 0) {
const classes = ['dropdown__options'];

classes.push(
`dropdown__options--${this.props.openDirection}-align`
);
const menuOptions = options.map(option => {
...
});
return (
<div className={ classNames.apply(null, classes) }>
<ul>
{ menuOptions }
</ul>
</div>
);
}

By simply iterating over the 2-level nested dropdown, I could have built the component in a fraction of the time; however, building this Nested Dropdown in an abstract way allows it to be easily reused in the future, despite having variations in design and use cases. Moreover, the point of the component-based architecture that underlies React emphasizes the concepts of autonomous, reusable pieces of user-interface.

And at the very least, it was a great exercise in using recursion! :)

Here is the final component:

Want to learn more about Tripping.com? Check out our team!

--

--