React
easy
mid
How would you build a React list with collapse and expand functionality?
Common build. Store expanded state as a Set<id> for O(1) toggle. Each row reads its expanded state and renders children when open. For 'only one open at a time' use a single id. For animation, use height: auto with transform tricks or framer-motion's AnimatePresence. Avoid storing expanded state inside the row component if you need controlled state from the parent.
6 min read·~25 min to think through
Bread-and-butter live coding question.
Independent collapse/expand
tsx
import { useState } from 'react';
type Item = { id: string; title: string; body: string };
function CollapsibleList({ items }: { items: Item[] }) {
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const toggle = (id: string) => {
setExpanded(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button
aria-expanded={expanded.has(item.id)}
onClick={() => toggle(item.id)}
>
{item.title}
</button>
{expanded.has(item.id) && <div>{item.body}</div>}
</li>
))}
</ul>
);
}Accordion (one at a time)
tsx
function Accordion({ items }: { items: Item[] }) {
const [openId, setOpenId] = useState<string | null>(null);
return (
<ul>
{items.map(item => {
const isOpen = openId === item.id;
return (
<li key={item.id}>
<button
aria-expanded={isOpen}
onClick={() => setOpenId(isOpen ? null : item.id)}
>
{item.title}
</button>
{isOpen && <div>{item.body}</div>}
</li>
);
})}
</ul>
);
}Memoized row + stable callbacks
For long lists, memoize the row and pass a stable toggle:
tsx
const Row = memo(function Row({
item, expanded, onToggle,
}: { item: Item; expanded: boolean; onToggle: (id: string) => void }) {
return (
<li>
<button aria-expanded={expanded} onClick={() => onToggle(item.id)}>
{item.title}
</button>
{expanded && <div>{item.body}</div>}
</li>
);
});Accessibility
aria-expandedon the trigger.aria-controlspointing to the panel id.- Keyboard: Enter/Space toggles; for accordions, Arrow keys move between headers.
Animation
Use a library — animating height in pure CSS is notoriously tricky.
tsx
import { AnimatePresence, motion } from 'framer-motion';
{isOpen && (
<AnimatePresence>
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
>
{item.body}
</motion.div>
</AnimatePresence>
)}Nested trees (file explorer)
For tree-shaped data, recurse:
tsx
function Tree({ node, expanded, toggle }: any) {
const isOpen = expanded.has(node.id);
return (
<div>
<button onClick={() => toggle(node.id)}>
{isOpen ? 'open' : 'closed'} {node.name}
</button>
{isOpen && node.children?.map((c: any) => (
<div style={{ marginLeft: 16 }} key={c.id}>
<Tree node={c} expanded={expanded} toggle={toggle} />
</div>
))}
</div>
);
}What interviewers look for
- Stable keys (
item.id, not index). - State held at the right level (parent if a sibling needs it).
- Accessibility (aria-expanded at minimum).
- Reasonable performance posture (memo + stable callbacks).
- Clean toggle logic (Set or single-id, no nested mutation).
Follow-up questions
- •How would you persist expanded state across page reloads?
- •How would you make this controlled vs uncontrolled?
- •What ARIA attributes are needed for a proper accordion?
Common mistakes
- •Toggling expanded state by mutating the existing Set — React won't detect the change.
- •Storing expanded as a boolean inside each row — sibling rows can't read it.
- •Animating height in pure CSS by setting height: auto — doesn't transition.
Performance considerations
- •Set lookup is O(1). For 10k+ items, memoize the row component so a toggle only re-renders the affected row. For deeply nested trees, lazy-render children — don't compute the full subtree if collapsed.
Edge cases
- •Filtering or sorting the list while items are expanded — preserve the Set across renders.
- •Very large lists with all expanded — performance hit; virtualize.
- •Keyboard navigation for nested trees — arrow keys must traverse parent/child.
Real-world examples
- •VSCode file explorer, Notion's nested blocks, Trello board collapse, Slack channel sections, every FAQ page. Radix UI's Accordion is the standard headless reference.
Senior engineer discussion
Senior framing: this question tests state-shape choice (Set vs single id vs boolean-per-row), API design (controlled vs uncontrolled), and a11y awareness. Build the simple version first, then explain how you'd extend for animation, persistence, controlled mode.