Recursive React Component Rendering
Recursion in React components is a powerful pattern for writing simple and clear code. If you’ve never tried it before or seen this pattern, it might sound a bit odd at first, but it’s really quite useful. I’m going to show here how it works, the benefits, and some things to look out for when using recursion inside React rendering.
What is Recursion?
Recursion is a programming tool in computing where a function calls itself during its execution. This can be done over and over again until some base condition is met that stops the recursion.
Here’s a quick example:
const launchRocket = (count) => {
if (count === 0) {
console.log('liftoff!');
} else {
console.log(`${count}...`);
launchRocket(count - 1);
}
};
launchRocket(10);
javascript
Until the base case is met, we continually make a recursive call to the same function, decreasing the count argument by 1. Once we count down to 0, the recursion ends, and our program is finished! Here’s the output for the launchRocket
function:
10...
9...
8...
7...
6...
5...
4...
3...
2...
1...
liftoff!
unknown
Recursion in React
Similar to how this works in traditional programming, recursion can also be used when rendering React components! Let’s look at an example of this pattern, based on the first vanilla Javascript one:
const RocketLaunch = (props) => {
if (props.count === 0) {
return 'liftoff!';
}
return (
<>
{props.count}...
<RocketLaunch count={props.count - 1} />
</>
);
};
<RocketLaunch count={10} />;
jsx
Here, the RocketLaunch
component renders the countdown text, and recursively renders itself, decreasing the count as we go until “liftoff!” is returned, and we end the recursion.
We can take a look at the transpiled code to understand how this works:
const RocketLaunch = (props) => {
if (props.count === 0) {
return 'liftoff!';
}
return React.createElement(
React.Fragment,
null,
props.count,
'...',
// This is the recursive call:
React.createElement(RocketLaunch, {
count: props.count - 1,
}),
);
};
React.createElement(RocketLaunch, {
count: 10,
});
javascript
Notice the recursive call in that code? We first call React.createElement(RocketLaunch, {count: 10})
, and in the recursion we call React.createElement(RocketLaunch, {count: props.count - 1})
. Doesn't look to different from vanilla Javascript, does it?
Let's take a look at how this component renders in the browser:
Adjust the slider to change the initial countdown value
<RocketLaunch count={10} />
That's enough of a proof of concept for me, so let's look at some real-world examples where this is useful.
Real-World Applications
We now know how to use recursion in React, but why would we want to do such a thing? This particular pattern is interesting to me because it has a few use-cases that are pretty different. There are many more out there, but these are some places I've used this pattern recently.
Heirarchial Data
Imagine we have a data structure like this, that represents a simple file tree:
{
name: 'site',
children: [{
name: 'home',
children: [{
name: 'index.html',
}, {
name: 'index.css'
}]
}, {
name: 'about',
children: [{
name: 'resume',
children: [{
name: 'index.html',
}]
}, {
name: 'contact',
children: [{
name: 'company',
children: [{
name: 'index.html'
}, {
name: 'index.js'
}]
}]
}]
}]
}
javascript
The following is a component that recursively renders itself down to each leaf node,
adding padding to the left to represent the heirarchy. The styling is done with Tailwind CSS
in my example code for simplicity. pl
and pb
here are shorthand for padding-left
and padding-bottom
.
const FileTree = (props: any) => {
const path = `${props.path ?? ""}/${props.node.name}`;
const hasChildren = props.node.children?.length > 0;
return (
<div className="pl-5">
<div className="flex pb-5 gap-x-5">
<FontAwesomeIcon icon={hasChildren ? faFolderOpen : faFile} />
<div>
{props.node.name} <span className>({path})</span>
</div>
</div>
{props.node.children?.map((child: any) => {
return (
<FileTree key={`${path}/${child.name}`} node={child} path={path} />
);
})}
</div>
);
};
jsx
The recursion here takes care of many concerns for us! The progressive indenting is handled automatically, and we don't need to worry about how many levels deep the tree goes. React render keys are progressively built for each node based on the path. In React, when mapping over an array of data to render components, each component needs a unique key. This is so React can keep track of which components have changed and need to be re-rendered.
We can render the file tree with the following code, passing in the data object built earlier:
<FileTree node={/* The data payload here */} />
jsx
Here's how that component renders:
Pagination and Infinite Scrolling
I recently rolled out a new feature on my blog - infinite scrolling on the blog list page. This was a fun feature to implement, and I used a recursive component to do it!
The first page of blog posts gets rendered server-side, and additional pages of data are fetched and parsed client-side. Additional pages are recursively rendered, only after the previous page has been finished and the last element has been scrolled into view.
It's a lot more complex than what I'll show in the following examples, but you can check out the complete code on GitHub to see how it all works.
As long as there is an additional page of posts to be rendered, the previous page will recursively render the next page. I do this by passing through the recursive calls some props to help:
const DynamicPosts = async (props) => {
const currentPage = props.previousPage + 1;
const nextPage = currentPage + 1;
const hasNextPage = nextPage <= pageCount;
// This is an oversimplification of the data fetching, but you get the idea
const pageData = await fetchPage(currentPage);
return (
<>
<PageData data={pageData} />
{hasNextPage ? (
<DynamicPosts
previousPage={currentPage}
pageCount={props.pageCount}
postsPerPage={props.postsPerPage}
remainingPosts={props.remainingPosts - pageData.length}
/>
) : null}
</>
);
};
jsx
I return a fragment here instead of using a wrapping element. This is important so that the recursive calls render all the
<PageData />
components as siblings. This is opposite to how it works in the file tree example, where we want each recursive call to render as a child of the previous call.
Here is the HTML that is generated by this component after scrolling through a few pages:
You can see here that the recursive calls render as siblings, and the DOM is structured
with a single parent for all the blog posts as the pages are fetched and rendered while the
user scrolls. Each element with the class last-element-page-*
represents the end of a page,
and the next page is recursively rendered. That element is also the one that tracks the visibility
of the end of the page to kick off the request for the next one.
Simplifying UI Components
While I was writing this post, Adam Wathan, the creator of Tailwind published a Tweet about this pattern and how it can be used conditionally based on children to create a simple checkbox component with or without a label:
Things to Watch Out For
I hope I've sufficiently shown you that this is a useful pattern to use in React, but there are a few things to watch out for when using recursion to render components.
Choose The Right Base Case
The base case must be hit in order to stop the recursion. If the base case is chosen incorrectly, the recursion may never end until your application crashes. You'll see something like this:
RangeError: Maximum call stack size exceeded
unknown
Let's revisit the rocket countdown example, with a twist. This time, we're in a hurry, and we want to count down by 2:
const RocketLaunch = (props) => {
if (props.count === 0) {
return 'liftoff!';
}
return (
<>
{props.count}...
<RocketLaunch count={props.count - 2} /> // Note the difference here
</>
);
};
<RocketLaunch count={10} />;
jsx
In this first example, we'll count down by 2 just fine: 10
, 8
, 6
, 4
, 2
, liftoff!
. But what if we change the initial count to 9?
<RocketLaunch count={9} />
jsx
Now, we get 9
, 7
, 5
, 3
, 1
, -1
, -3
, -5
, -7
, -11
and so on, forever! This is because the base case is never hit, and the recursion never ends.
A better base case would have been:
if (props.count <= 0) {
return 'liftoff!';
}
javascript
Now, if our counter "skips" 0, we'll still hit the base case and end the recursion.
This issue can be particularly tricky, because the base case may work fine in some or even most situations, but when taking in user input, it might fail! Always be sure to think carefully about the base case when using recursion, and be aware of what unexpected input might do.
Here's the same interactive example from the first RockerLaunch component, but using the new base-case and a countdown interval of 2:
<RocketLaunch count={10} />
Be Aware of Excessive DOM Nodes
As I touched on in my real-world examples, you need to be careful about how the DOM is structured when using recursion. In the file tree example, we want each recursive call to render as a child of the previous call. In the infinite scrolling example, we want each recursive call to render as a sibling of the previous call.
The infinite scrolling could have been implemented with each page rendering inside its own container, but that would have introduced some complexity in the CSS, and many additional elements that aren't necessary.
The same holds true for our countdown example. It might be fine to create a new element wrapping each number when we only have 10 recursive calls, but if this takes user input and doesn't have a sane maximum, what would happen if a user wanted to count down from 10,000? That's already a lot of elements, but now we'd not only have 10,000 additional elements we don't need, but the last one would be at a depth of 10,000! This would be a nightmare for the browser to render:
<span>
10000...
<span>
9999...
<span>
9998...
<span>
9997...
<span>
9996...
<!-- And so on... -->
</span>
</span>
</span>
</span>
</span>
</span>
html
If you want to read more about browser performance and maximum DOM depth, this Chrome for Developers article is a great resource.
Final Thoughts
Recursion is a powerful tool in programming, and it can be used in React to simplify component rendering. It can also be used incorrectly to greatly increase complexity and cause performance issues or hard-to-debug crashes. Always be sure that you're using the right tool for the job, and that you're using it correctly!
Hopefully this post has helped you understand how to use recursion in React, and some things to watch out for when doing so. If you have any questions or comments, let's discuss on X (Twitter)!