machine coding11
react ui components
recursionnested-stateimmutability

File explorer tree

Render a nested folder structure with expandable and collapsible nodes.

medium1 min readsolution

What it tests

Recursive rendering plus the harder half — updating one node deep inside a nested structure without mutating it.

The approach

TreeNode renders itself for each child, passing down the path it took to get there as an array of indices:

<TreeNode data={child} path={[...path, idx]} onToggle={onToggle} />

State stays in one place at the top. Toggling clones the tree, walks the path to the target node and flips isOpen:

const newTree = structuredClone(prev);
let node = newTree;
for (let i = 0; i < path.length; i++) node = node.children[path[i]];
node.isOpen = !node.isOpen;

Passing the path rather than the node itself is what makes this work — a node reference alone gives you no way back to its position in the new clone.

Where it usually goes wrong

  • Mutating the node directly. React sees the same top-level reference and doesn't re-render.
  • Local isOpen in each node. Fine until something needs to collapse all, which nothing can then do.
  • structuredClone on a large tree. Correct and cheap to write, but it copies everything on every click; a path-aware update that only clones the spine scales better.

Full solution

3 files from src/folders/File-manager.

index.jsx

import "./styles.css";
 
export function FolderStr({ data, path, onToggle }) {
  return <TreeNode data={data} path={path} onToggle={onToggle} />;
}
 
function TreeNode({ data, path, onToggle }) {
  const isFolder = data.type === "folder";
  const isOpen = data.isOpen ?? false;
 
  function handleClick() {
    onToggle(path);
  }
 
  return (
    <div className="tree">
      <Box data={data} handleClick={handleClick} />
 
      {isFolder && isOpen && (
        <div className="tree-child">
          {data.children?.map((child, idx) => (
            <TreeNode
              key={idx}
              data={child}
              path={[...path, idx]}
              onToggle={onToggle}
            />
          ))}
        </div>
      )}
    </div>
  );
}
 
function Box({ data, handleClick }) {
  const isFile = data.type === "file";
  const isOpen = data.isOpen ?? false;
 
  return (
    <div className="box" onClick={handleClick}>
      {isFile ? "📄" : isOpen ? "📂" : "📁"} {data.name}
    </div>
  );
}

config.js

export const data = {
  id: "1",
  name: "root",
  type: "folder",
  children: [
    {
      id: "2",
      name: "src",
      type: "folder",
      children: [
        { id: "3", name: "App.jsx", type: "file" },
        { id: "4", name: "index.js", type: "file" },
        { id: "5", name: "throttle.js", type: "file" },
        {
          id: "6",
          name: "File-manager",
          type: "folder",
          children: [
            { id: "7", name: "index.jsx", type: "file" },
            { id: "8", name: "config.js", type: "file" },
            { id: "9", name: "index.css", type: "file" },
          ],
        },
        { id: "10", name: "hooks", type: "folder" },
      ],
    },
  ],
};

styles.css

.tree {
  display: flex;
  flex-direction: column;
  padding-left: 20px;
}
.tree li {
  list-style: none;
}
share this post