DEV Community

loading...
Cover image for TIL: You can include a react component inside itself

TIL: You can include a react component inside itself

inbn profile image Iain Bean ・2 min read

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

Discussion

pic
Editor guide