react ui components
recursionnested-stateimmutability
File explorer tree
Render a nested folder structure with expandable and collapsible nodes.
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
isOpenin each node. Fine until something needs to collapse all, which nothing can then do. structuredCloneon 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