How to Create a Recursive React Component

Harsh MaroliaHarsh Marolia
5 min read

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;
0
Subscribe to my newsletter

Read articles from Harsh Marolia directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harsh Marolia
Harsh Marolia