I recently converted the posts in one of my Gatsby sites from regular markdown to MDX. During this process, I had to change the way I render the Table of Contents for each post. Instead of a block of HTML passed to the dangerouslySetInnerHTML
function, I'm now working with an array of objects, each representing a level 2 heading:
[
{
url: '#description',
title: 'Description'
},
{
url: '#layout',
title: 'Layout'
}
// More headings here…
];
To render a list of these headings, we can loop through them using the Array.map()
method:
<ul>
{items.map(item => (
<li key={item.title}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
This works fine if we’re only rendering one level of heading, but what if we also wanted to show level 3 headings? Fortunately, the tableOfContents
provided by gatsby-plugin-mdx includes any child headings in a nested items
object. e.g.
{
url: '#markup',
title: 'Markup',
items: [
{
url: '#approach-1-heading-with-button',
title: 'Approach 1: Heading with Button'
},
{
url: '#approach-2-summary-and-details',
title: 'Approach 2: Summary and Details'
}
]
}
How do we render these child headings? One answer is to nest the same logic again inside our existing loop:
const TableOfContents = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.title}>
<a href={item.url}>{item.title}</a>
{/* Begin nested loop */}
{item.items && item.items.length > 0 && (
<ul>
{items.map(item => (
<li key={item.title}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
);
Unfortunately, not only is this starting to get unwieldy and repetitive, it also has its limitations: what if we want to render three levels of heading: h2, h3 and h4s? This is a problem that can be solved using recursion. If we make a new TableOfContentsItem
component for rendering our list items, we can give it the ability to call itself if it needs to render any children:
const TableOfContentsItem = ({ item }) => {
const nestedItems = (item.items || []).map(nestedItem => {
return <TableOfContentsItem item={nestedItem} key={nestedItem.title} />;
});
return (
<li key={item.title}>
<a href={item.url}>{item.title}</a>
{nestedItems.length > 0 && <ul>{nestedItems}</ul>}
</li>
);
};
Note how, when it needs to, the TableOfContentsItem
loops through any child items, rendering each one as another TableOfContentsItem
. Our TableOfContents
component now only needs to include it once, inside the outer-most ul
:
const TableOfContents = ({ items }) => (
<ul>
{items.map(item => (
<TableOfContentsItem item={item} key={item.title} />
))}
</ul>
);
And that’s it: we can now handle any level of nesting. This technique comes in handy for all sorts of complex navigation and it’s not only for React: the same pattern can be achieved using template macros in languages such as Nunjucks or Twig.
Photo by Iza Gawrych on Unsplash
Top comments (0)