Back to Machine Coding
Machine Coding
easy
mid

How would you build a file explorer UI component?

A recursive tree: a Node component that renders itself for children. State tracks which folders are expanded (a Set of ids). Folders toggle open/closed, files are leaves. Watch: recursion, expanded-state management, accessibility (role=tree/treeitem, keyboard nav), and lazy-loading large trees.

5 min read·~25 min to think through

A file explorer is the canonical recursive component problem — a tree that renders itself.

The data and the recursive component

jsx
// data: { id, name, type: "file" | "folder", children?: Node[] }

function TreeNode({ node, expanded, onToggle, depth = 0 }) {
  const isFolder = node.type === "folder";
  const isOpen = expanded.has(node.id);

  return (
    <li role="treeitem" aria-expanded={isFolder ? isOpen : undefined}>
      <div style={{ paddingLeft: depth * 16 }}
           onClick={() => isFolder && onToggle(node.id)}>
        {isFolder ? (isOpen ? "📂" : "📁") : "📄"} {node.name}
      </div>

      {isFolder && isOpen && (
        <ul role="group">
          {node.children.map((child) => (
            <TreeNode key={child.id} node={child}     {/* RECURSION */}
              expanded={expanded} onToggle={onToggle} depth={depth + 1} />
          ))}
        </ul>
      )}
    </li>
  );
}

The key idea: TreeNode renders TreeNode for each child — recursion handles arbitrary nesting depth.

Expanded state — lifted, a Set of ids

jsx
const [expanded, setExpanded] = useState(new Set());
const toggle = (id) =>
  setExpanded((prev) => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;                              // new Set — immutable update
  });

Keep expanded state lifted to the parent (a Set of expanded folder ids), not as local state inside each node — so it survives, can be controlled (expand-all, restore from URL), and one node doesn't own truth.

What's being graded

  • Recursion — the component renders itself; clean base case (files are leaves).
  • Expanded state management — a Set, lifted, updated immutably.
  • Stable keys — node ids.
  • Accessibility — it's a tree: role="tree" on the root, role="treeitem" per node, role="group" for children, aria-expanded on folders. Keyboard nav: Up/Down to move, Right to expand/enter, Left to collapse/go to parent, Enter to open a file.
  • Indentation by depth.

Scaling considerations

  • Lazy-loading — for huge trees (a real filesystem), don't load all children upfront; fetch a folder's children when it's first expanded.
  • Virtualization — a deeply expanded tree can have thousands of visible rows; flatten the visible nodes and virtualize.
  • Memoize TreeNode — so toggling one folder doesn't re-render the whole tree.
  • Optional: selection, drag-and-drop to move, context menus, search/filter.

The framing

"It's a recursive component — a TreeNode that renders TreeNode for each child, so arbitrary depth just works, with files as the leaf base case. Expanded state is a Set of folder ids lifted to the parent and updated immutably — not local state per node, so it's controllable and persistent. The grading is recursion plus accessibility — the WAI-ARIA tree pattern with role=tree/treeitem/group, aria-expanded, and arrow-key navigation. For a real filesystem I'd lazy-load children on first expand and virtualize the visible rows."

Follow-up questions

  • How does the recursion handle arbitrary nesting depth?
  • Why lift expanded state to the parent instead of per-node?
  • What ARIA roles does the tree pattern need?
  • How would you handle a filesystem too large to load upfront?

Common mistakes

  • Local expanded state inside each node — can't control or persist it.
  • Mutating the Set instead of creating a new one.
  • No base case clarity / mishandling files vs folders.
  • Missing tree ARIA roles and keyboard navigation.
  • Loading and rendering a huge tree all at once.

Performance considerations

  • Memoize TreeNode so toggling one folder doesn't re-render siblings; lazy-load children on expand; virtualize the flattened visible-node list for large trees so the DOM stays bounded.

Edge cases

  • Empty folders.
  • Very deep nesting.
  • Huge trees needing lazy-load and virtualization.
  • Circular references in the data (shouldn't happen, but guard).
  • Keyboard nav across collapsed/expanded boundaries.

Real-world examples

  • VS Code's file explorer, OS file browsers, nested comment threads.
  • Category/menu trees in admin UIs.

Senior engineer discussion

Seniors implement clean recursion with a clear base case, lift expanded state as an immutably-updated Set, apply the WAI-ARIA tree pattern with keyboard nav, and proactively raise lazy-loading, virtualization, and memoization for large trees.

Related questions