Frontend
easy
mid
Build a collapsible folder tree with nested checkboxes.
Render a recursive tree component; track expanded nodes and checkbox state. Checkboxes are tri-state: checking a folder cascades to children, and a parent shows indeterminate when children are mixed. Keep node state in a normalized structure and make it keyboard-accessible.
7 min read·~30 min to think through
This is a recursion + state-propagation problem. Two independent concerns: expand/collapse and tri-state checkboxes.
Data model
js
// Normalized for easy updates:
nodes = {
'1': { id: '1', name: 'src', childIds: ['2','3'], parentId: null },
'2': { id: '2', name: 'index.js', childIds: [], parentId: '1' },
}Or a nested tree if it's read-mostly. Normalized makes propagation updates cleaner.
Recursive rendering
jsx
function TreeNode({ id }) {
const node = useNode(id);
const isExpanded = useExpanded(id);
const checkState = useCheckState(id); // 'checked' | 'unchecked' | 'indeterminate'
return (
<li role="treeitem" aria-expanded={node.childIds.length ? isExpanded : undefined}>
{node.childIds.length > 0 && (
<button onClick={() => toggleExpand(id)} aria-label="toggle">
{isExpanded ? '▼' : '▶'}
</button>
)}
<Checkbox state={checkState} onChange={() => toggleCheck(id)} />
<span>{node.name}</span>
{isExpanded && node.childIds.map((cid) => <TreeNode key={cid} id={cid} />)}
</li>
);
}The tri-state checkbox logic
The whole problem lives here:
- Check/uncheck a folder → cascade down: every descendant takes the new value.
- Toggle any node → recompute ancestors up: a parent is
checkedif all children are checked,uncheckedif none,indeterminateotherwise. indeterminateis not an HTML attribute — set it imperatively:inputRef.current.indeterminate = true.
Two clean implementations:
- Derived (recommended): store only leaf checked-state; compute folder state on read by walking children. No sync bugs — folders are pure derived state.
- Stored + propagated: store every node's state, and on each toggle run a down-cascade and an up-recompute. Faster reads, but you must keep it consistent.
Accessibility (don't skip — interviewers check)
role="tree"on the container,role="treeitem"on nodes,role="group"on child lists.aria-expandedon folders,aria-checked("mixed"for indeterminate).- Keyboard: Arrow keys to move/expand/collapse, Space to toggle the checkbox, roving
tabindexso Tab enters the tree once.
Performance
- Large trees → only render expanded subtrees (already done above), and virtualize the flattened visible list if it's huge.
React.memoonTreeNodeso toggling one node doesn't re-render siblings.
Follow-up questions
- •Why is storing only leaf state and deriving folder state more robust?
- •How do you set the indeterminate state on a checkbox?
- •What ARIA roles and keyboard interactions does a tree need?
- •How would you virtualize a very large expanded tree?
Common mistakes
- •Trying to use indeterminate as a JSX attribute — it must be set imperatively via ref.
- •Storing folder checked-state and letting it drift from children.
- •No keyboard support or ARIA roles.
- •Re-rendering the whole tree on every single toggle.
Performance considerations
- •Only render expanded subtrees; memoize TreeNode so a toggle doesn't re-render siblings. For thousands of visible nodes, flatten and virtualize. Deriving folder state is O(children) per read but eliminates a whole class of sync bugs.
Edge cases
- •Empty folders — checking one with no children.
- •Very deep nesting hitting recursion/render limits.
- •Mixed selection states deep in the tree updating ancestors correctly.
- •Disabled nodes that shouldn't be affected by parent cascade.
Real-world examples
- •File explorers in VS Code / GitHub's file tree.
- •Permission/scope pickers, category selectors with parent-child selection.
Senior engineer discussion
Seniors call out the derived-vs-stored tradeoff for folder state (derived eliminates sync bugs), handle the indeterminate-via-ref detail without prompting, and treat accessibility (tree/treeitem roles, aria-checked='mixed', keyboard nav) as part of the spec, not an extra. They also flag virtualization for large trees and memoization to contain re-renders.
Related questions
Frontend
Medium
6 min
Frontend
Easy
5 min