How to Create a Recursive React Component
When working with complex data structures, such as nested folders or hierarchical categories, you might need a recursive component in React. This allows you to render nested elements dynamically, making your UI more adaptable and your code more modular.
In this blog, I'll walk you through creating a recursive React component to display a file system structure. The component will handle both folders and files, and we'll use simple icons to represent each type.
The Data Structure
First, let's define the data structure that we'll be rendering. This is a tree-like structure where each node can be a file or a folder containing other nodes.
type Node = {
name: string;
nodes?: Node[];
};
let nodes: Node[] = [
{
name: "Home",
nodes: [
{
name: "Movies",
nodes: [
{
name: "Action",
nodes: [
{
name: "2000s",
nodes: [
{ name: "Gladiator.mp4" },
{ name: "American-Beauty.mp4" },
],
},
{ name: "2010s", nodes: [] },
],
},
{ name: "Comedy", nodes: [{ name: "2000s", nodes: [] }] },
],
},
{
name: "Music",
nodes: [
{ name: "Rock", nodes: [] },
{ name: "Classical", nodes: [] },
],
},
{ name: "Pictures", nodes: [] },
{ name: "Document", nodes: [] },
{ name: "password.txt" },
],
},
];
The Recursive Component
Next, we'll create the FileSystemItem
component. This component will render a folder or a file; if the item is a folder, it will recursively render its children.
import * as React from "react";
import FolderIcon from "./icons/folderIcon";
import FileIcon from "./icons/fileIcon";
import ChevronIcon from "./icons/chevronIcon";
import { useState } from "react";
function FileSystemItem({ node }: { node: Node }) {
const [isOpen, setIsOpen] = useState(false);
return (
<li className="my-1.5" key={node.name}>
<span className="flex item-center gap-1.5">
{node.nodes && node.nodes.length > 0 && (
<button onClick={() => setIsOpen(!isOpen)}>
<ChevronIcon
className={`size-4 text-gray-500 ${isOpen ? "rotate-90" : ""}`}
/>
</button>
)}
{node.nodes ? (
<FolderIcon
className={`size-6 text-sky-500 ${
node.nodes.length === 0 ? "ml-[22px]" : ""
}`}
/>
) : (
<FileIcon className="ml-[22px] size-6 text-grey-900" />
)}
{node.name}
</span>
{isOpen && (
<ul className="pl-6">
{node.nodes?.map((childNode) => (
<FileSystemItem node={childNode} key={childNode.name} />
))}
</ul>
)}
</li>
);
}
Rendering the Component
Finally, let's render the entire file system using our recursive component within the App
component.
export default function App() {
return (
<div className="p-8 max-w-sm mx-auto">
<ul className="pl-6">
{nodes.map((node) => (
<FileSystemItem node={node} key={node.name} />
))}
</ul>
</div>
);
}
Conclusion
Recursive components are a powerful way to handle nested data in React. In this example, we created a simple file explorer that can handle an arbitrary depth of nested folders. The component is clean, modular, and easy to maintain.
I hope this tutorial helps you understand how to implement recursion in your React components. Feel free to customize this code and experiment with different structures!
Full Code + Icons
// App.tsx
import * as React from "react";
import FileSystemItem from "./FileSystemItem";
type Node = {
name: string;
nodes?: Node[];
};
let nodes: Node[] = [
{
name: "Home",
nodes: [
{
name: "Movies",
nodes: [
{
name: "Action",
nodes: [
{
name: "2000s",
nodes: [
{ name: "Gladiator.mp4" },
{ name: "American-Beauty.mp4" },
],
},
{ name: "2010s", nodes: [] },
],
},
{ name: "Comedy", nodes: [{ name: "2000s", nodes: [] }] },
],
},
{
name: "Music",
nodes: [
{ name: "Rock", nodes: [] },
{ name: "Classical", nodes: [] },
],
},
{ name: "Pictures", nodes: [] },
{ name: "Document", nodes: [] },
{ name: "password.txt" },
],
},
];
export default function App() {
return (
<div className="p-8 max-w-sm mx-auto">
<ul className="pl-6">
{nodes.map((node) => (
<FileSystemItem node={node} key={node.name} />
))}
</ul>
</div>
);
}
// FileSystemItem.tsx
import FolderIcon from "./icons/folderIcon";
import FileIcon from "./icons/fileIcon";
import ChevronIcon from "./icons/chevronIcon";
import { useState } from "react";
type Node = {
name: string;
nodes?: Node[];
};
export default function FileSystemItem({ node }: { node: Node }) {
const [isOpen, setIsOpen] = useState(false);
return (
<li className="my-1.5" key={node.name}>
<span className="flex item-center gap-1.5">
{node.nodes && node.nodes.length > 0 && (
<button onClick={() => setIsOpen(!isOpen)}>
<ChevronIcon
className={`size-4 text-gray-500 ${isOpen ? "rotate-90" : ""}`}
/>
</button>
)}
{node.nodes ? (
<FolderIcon
className={`size-6 text-sky-500 ${
node.nodes.length === 0 ? "ml-[22px]" : ""
}`}
/>
) : (
<FileIcon className="ml-[22px] size-6 text-grey-900" />
)}
{node.name}
</span>
{isOpen && (
<ul className="pl-6">
{node.nodes?.map((node) => (
<FileSystemItem node={node} key={node.name} />
))}
</ul>
)}
</li>
);
}
Icons
// ChevronIcon
const ChevronIcon = (props: any) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={`${props.className}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
);
};
export default ChevronIcon;
// FileIcon
const FileIcon = (props: any) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={`${props.className}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
);
};
export default FileIcon;
// FolderIcon
const FolderIcon = (props: any) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={`${props.className}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z"
/>
</svg>
);
};
export default FolderIcon;
Subscribe to my newsletter
Read articles from Harsh Marolia directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by