AniUI Academy

Build a file tree with lazy loading

hardUI ComponentsHigh freq+240 XP

Build a file tree. Three root nodes are provided (`src/`, `tests/`, `docs/`), each a folder.

Clicking a folder toggles it open/closed. On first open, its children are loaded (simulated with a short setTimeout) and cached — subsequent opens don't re-fetch.

The tree component is itself recursive — a folder renders its children by rendering more of itself.

Examples

// Initial: three collapsed folders — src/, tests/, docs/

// Click toggle-src
// → <span data-testid="loading-src">Loading…</span>
// → (150ms later)
// → <ul>
//     <li data-testid="node-src.App.js">App.js</li>
//     <li data-testid="node-src.components">…</li>
//   </ul>

// Collapse, re-open → children appear instantly, no loading.

First open loads + caches; subsequent opens render from cache.

function TreeNode({ node, path }) {
  const [isOpen, setIsOpen] = useState(false);
  const [isLoaded, setIsLoaded] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  // ...
  return (
    <li data-testid={`node-${path}`}>
      <button onClick={toggle}>{node.name}</button>
      {isOpen && (isLoading
        ? <span data-testid={`loading-${path}`}>Loading…</span>
        : <ul>{node.children.map((c) => <TreeNode node={c} />)}</ul>
      )}
    </li>
  );
}

Recursive component: each folder renders a `<ul>` of `TreeNode` for its children.

What it has to do

  • Each node is a `<li data-testid="node-{path}">`, where `{path}` is the dot-separated path from the root (e.g. `src.components.Button.tsx`).
  • Each folder's label is a `<button data-testid="toggle-{path}">` showing the folder name.
  • Clicking a folder toggles its open state.
  • On first open, show `<span data-testid="loading-{path}">Loading…</span>` while children fetch; replace with the children `<ul>` once loaded. Second open does not re-show the loading state.
  • A collapsed folder's children are not present in the DOM at all (unmounted, not just hidden).
  • Each file (leaf) renders its name as plain text — files are not clickable toggles.

Notes

  • Keep `isOpen` and `isLoaded` as separate state per node — collapsing doesn't throw away the cache, so re-open is instant.
  • Collapsed children should be unmounted, not CSS-hidden — otherwise a very deep tree builds the entire DOM upfront and defeats lazy loading.
  • In production, swap the local `setTimeout` for a real fetch keyed on the folder path, and show a retry affordance if the request fails. The loading placeholder is the same.
  • Accessibility: expose each folder as `role="treeitem"` inside `role="tree"`, with `aria-expanded` and `aria-level`; arrow keys should move focus between siblings/parents.
Asked atMicrosoftAtlassianGitLabFigmaLinear

Stuck? The react course covers the ideas this problem is built on.

import { useState } from "react";

const ROOT = [
  {
    name: "src",
    type: "folder",
    children: [
      { name: "App.js", type: "file" },
      {
        name: "components",
        type: "folder",
        children: [
          { name: "Button.tsx", type: "file" },
          { name: "Modal.tsx", type: "file" },
        ],
      },
    ],
  },
  { name: "tests", type: "folder", children: [{ name: "App.test.js", type: "file" }] },
  { name: "docs", type: "folder", children: [{ name: "README.md", type: "file" }] },
];

function TreeNode({ node, path }) {
  // TODO: for a folder, toggle open on click, show "Loading…" on first open
  // (for ~150ms via setTimeout), then render children. For a file, render the
  // name. Both are wrapped in <li data-testid={`node-${path}`}>.
  return (
    <li data-testid={`node-${path}`}>
      {node.name}
    </li>
  );
}

export default function App() {
  return (
    <ul style={{ fontFamily: "sans-serif", padding: 16, listStyle: "none" }}>
      {ROOT.map((n) => (
        <TreeNode key={n.name} node={n} path={n.name} />
      ))}
    </ul>
  );
}

5 tests · +240 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.