TIL: You can include a react component inside itself
Iain Bean
Posted on September 6, 2020
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
Posted on September 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.