Three types of recursive React components

You have probably used recursion by now on other, more data centring, problems, but it can be a valid approach when coding UI components. Here’s a simple take on the topic, using React.

When I think of recursive components three main cases that are worth talking about come to mind.

Purely visual

These components are mainly for flashy display. Some of them can have the self-similarity quality of geometrical fractals but in day to day web development they are required to be less visually complex than a fractal. One would consider this method when creating unique icons or backgrounds, or even wacky plots.

Taking the common element, the div, a minimal code example for this case would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const computeColor = (currentDept, maxDepth) => {
const green = 255 * currentDept / maxDepth;
const opacity = 0.1 + currentDept / maxDepth * 0.9;
return `rgba(0, ${green}, 0, ${opacity})`
};

const Squares = ({ currentDept = 0, maxDepth = 10 }) => {
// stop condition
if (currentDept >= maxDepth) {
return null;
}
return <div style={{
backgroundColor: computeColor(currentDept, maxDepth),
width: 100, height: 100, left: 30,
transform: `rotate(${currentDept / maxDepth * 45}deg)`,
transformOrigin: '0% 100%',
position: 'relative',
}}>
<Squares currentDept={currentDept + 1} maxDepth={maxDepth} />
</div>
};

// and then call it where ever as
<Squares currentDept={0} maxDepth={5} />

The component paints a div with dome specific styles dependent on the current depth and then calls itself with an incremented depth counter. As in the case of traversing a list or a tree, this can be done iteratively instead of recursively, for a small depth value performance is not an worthwhile issue and thus it becomes a matter of which approach is more concise in the developer’s eyes.

The visual output looks as follows:







Graph data with constant display

This case has actual more real life use cases and it is the easiest case when it comes to recursive renderings. Consider having a tree or graph whose nodes represent, let’s say, the product categories in a shop and the end user would need to navigate between them. Any of these nodes can be rendered by the same panel, all being orchestrated by React’s state. I would say this is a good approach on mobile screens.

Notice that here you do not need to recursively call the component.

To enable back navigation we need a reference to each node’s parent. The approach I used is a prior parsing of data and attaching the parent to each object. For this to work, it’s important to use the same object references as this provides the freedoms to navigate the graph. I would advise to attach the parents somewhere out of the rendering flow, right after data fetching or in a useEffect hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// pure component that renders a given node
const GraphDisplay = ({ node, onNodeClick }) => {
return <div>
{node.children?.map(c =>
<button
key={c.text}
onClick={() => onNodeClick(c)}>
{c.text}
</button>
)}
{node.parent ?
<button
onClick={() => onNodeClick(node.parent)}>
Go back to {node.parent.text}
</button>
: <div />
}
</div>;
};

// stateful component that keeps track of the current node,
// with a minimal (and optional) display of it's data
const GraphContainer = ({ node: graphNode }) => {
const [node, setNode] = useState(graphNode);

return <>
<h3>Viewing {node.text}</h3>
<GraphDisplay node={node} onNodeClick={n => setNode(n)} />
</>;
};

// declaration of the helper function that will link nodes to their parents
const attachParents = (node, parent) => {
node.parent = parent;
if (!node.children)
return;
for (const c of node.children) {
attachParents(c, node);
}
};
// ...
// actual data
const root = {
text: 'Home', children: [{
text: 'Shirts', children: [
{ text: 'T-Shirts' },
{ text: 'Dress Shirts' }]
}, {
text: 'Coats', children: [
{ text: 'Trench Coats' },
{ text: 'Rain Coats' }]
}]
};

// ...
attachParents(root); // do this out of the rendering loop, preferably

// ...
// rendering of the main component with enhanced data
<GraphContainer node={root} />

The code will behave as (with some minimal styles):

Graph data with unfolding display

This case is similar with the above one, only that it keeps the whole hierarchy path on screen. Almost all desktop programs implement this in menu displays. The code will use the category data from the previous example, but with a T-Shirts being split into a couple more sections.

Some key points for this case would be that now the component does call itself because we need to have multiple such visual instances on the screen at the same time. State also holds the child for which to further expand its children, and not the current node, and we don’t need a parent attached to each node because there is we don’t deal with back pagination in this case. In short, you render the a child the same way you rendered the parent node.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// pure component that renders a given node's children
const GraphMenuDisplay = ({ node, onNodeClick }) => {
if (!node.children || node.children?.length === 0) {
return null;
}
return <div>
{node.children.map(c =>
<button
key={c.text}
onClick={() => onNodeClick(c)}>
{c.text}
</button>
)}
</div>;
};

// the stateful container that controls what child of the current node to expand
const GraphMenuContainer = ({ node: graphNode }) => {
const [child, setChild] = useState(null);

const handleNodeClick = n => {
const actualChild = graphNode?.children?.find(c => c === n);
if (actualChild) {
setChild(actualChild);
} else {
setChild(null);
}
};

useEffect(() => {
// the component can remain mounted with a previous/stale state
// therefore it needs to update if the child does not belong to the current node
if (child === null)
return;
const actualChild = graphNode?.children?.find(c => c === child);
if (!actualChild)
setChild(null);
}, [child, graphNode]);

return <>
<GraphMenuDisplay node={graphNode} onNodeClick={handleNodeClick} />
{child && <GraphMenuContainer node={child} />}
</>;
};
// ...
// actual data shape
const menuRoot = {
text: 'Home', children: [{
text: 'Shirts', children: [
{ text: 'T-Shirts', children: [
{ text: 'Printed' },
{ text: 'Plain' }]
},
{ text: 'Dress Shirts' }]
}, {
text: 'Coats', children: [
{ text: 'Trench Coats' },
{ text: 'Rain Coats' }]
}]
};
//...
// usage in app
<GraphMenuContainer node={menuRoot} />

Again, the a minimal styled display could look like this.

Of course, you are free to extend the handling of the item selection the way it fits your needs.

Conclusion

In a more general sense many of use have already build similar stuff, like breadcrumbs and filters that would show the same page layout but seeded by another set of items.

The mentioned cases can be extended and combined endlessly once you get the gist of it. Most likely, there is a fair amount of work to tailor for specific requirements and visual goals, but I hope this post will be a source of initial inspiration.

Thumbnail image copyright © Chris Foss.