We use cookies for site analytics. Accept to help us understand how the site is used. See our Privacy Policy for details.
A medium frontend problem, graded against 5 test cases (3 of them hidden).
React and browser-platform problems - hooks, event handling, and rendering behaviour.
Reach for it when you see: A component, hook, or DOM-behaviour question rather than an algorithm.
More Frontendproblems →```tsx
export function VirtualList({ rows, rowHeight = 50, overscan = 5 }: { rows: Row[]; rowHeight?: number; overscan?: number }) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [viewportHeight, setViewportHeight] = useState(600);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
setViewportHeight(el.clientHeight);
const onScroll = () => setScrollTop(el.scrollTop);
el.addEventListener("scroll", onScroll, { passive: true });
return () => el.removeEventListener("scroll", onScroll);
}, []);
const { start, end } = visibleRange({ scrollTop, viewportHeight, rowHeight, totalRows: rows.length, overscan });
const visibleRows = rows.slice(start, end);
return (
<div ref={containerRef} style={{ height: 600, overflow: "auto" }}>
{/* Tall spacer to keep scrollbar accurate */}
<div style={{ height: rows.length * rowHeight, position: "relative" }}>
{visibleRows.map((row, i) => (
<div
key={row.id}
style={{ position: "absolute", top: (start + i) * rowHeight, height: rowHeight, width: "100%" }}
>
{row.label}
</div>
))}
</div>
</div>
);
}
```
Why virtualization wins: rendering 50,000 DOM nodes destroys frame budgets (each `<div>` with text is ~3-5KB of memory, and React's reconciler walks them). Virtualization caps the rendered DOM at `(viewport / rowHeight) + 2 * overscan` rows (typically 20-50), so cost is independent of list size.
Variable-height rows: much harder. Either measure rows post-mount and cache (TanStack Virtual, react-window's `VariableSizeList`), or use estimates with on-the-fly correction. The fixed-height case shown here is the easy one.
Pitfalls:
- `{ passive: true }` on the scroll listener avoids blocking native scrolling.
- `position: absolute` rows mean the spacer height drives the scrollbar.
- The `key` should be stable per row (`row.id`, not the index) so React doesn't tear down rows when the visible window shifts.
The full reference solution in every supported language stays in the editor above - reveal it there once you have had a real attempt.
These apply to the pattern as a whole, not just this problem.