# Virtualized List

> Build a virtualized list that renders only the rows visible in the scroll viewport (plus overscan) instead of mounting all 10,000 contacts, using fixed row heights and windowing math.

- Difficulty: intermediate
- Tags: useState, useLayoutEffect, useMemo, useRef, performance
- Estimated time: 60 minutes
- Tests: 12
- Requires subscription: yes

## Overview

Build a virtualized list that mounts only the rows visible in the scroll viewport (plus a small overscan) instead of rendering all 10,000 contacts at once, using fixed row heights and windowing math.

## Requirements

- The app renders a virtualized directory of 10,000 contacts.
- Only a small window of rows is mounted in the DOM — far fewer than the 10,000 contacts in the dataset.
- The first contacts are rendered on load (including `data-testid="virtual-row-1"`).
- The scroll container (`data-testid="virtual-viewport"`) scrolls through the entire dataset, not just the mounted rows.
- The scrollable area is sized to the full dataset (`data-testid="virtual-total"`), so the scrollbar spans the whole list.
- Scrolling down unmounts the rows above the viewport and mounts the rows that enter it.
- Scrolling back to the top restores the first rows.
- Scrolling to the bottom mounts the last contact.
- A few rows above and below the visible window are mounted too (overscan), more than the number that fit on screen.
- The stats line (`data-testid="virtual-stats"`) shows the first and last mounted row numbers: `Showing X–Y of 10000`.
- Each row (`data-testid="virtual-row-<id>"`) shows the contact's name and email.
- No rows are mounted before the first contact (indices never go negative).

## Notes

- `data.ts` and `Components.tsx` are read-only. `VirtualViewport` is the scroll box and calls `onScroll(scrollTop)`; `VirtualRow` is a `ROW_HEIGHT`-tall row that shows the contact's name and email; `VirtualStats` shows `Showing X–Y of N`. `ROW_HEIGHT` is exported from `Components`; derive the total with `CONTACTS.length`.
- Put all the windowing logic in `App.tsx`: track `scrollTop`, measure the viewport's `clientHeight`, and compute the range of rows to mount.
- Measure the viewport height in a layout effect (`viewportRef.current.clientHeight`); the viewport is styled `h-96`.
- The mounted rows must stay visually aligned with their position in the dataset as you scroll.
- Keep rows keyed by `contact.id`.
- Clamp the range so overscan never renders rows before the first or after the last contact (indices never go negative).
- The tests simulate the viewport by mocking `clientHeight` and dispatching `scroll` events with a fake `scrollTop`.

## Tests

01. renders the app title
02. only mounts a small window of rows, not the whole dataset
03. renders the first contacts on load
04. sizes the scrollable area to the full dataset
05. shows the first and last rendered rows in the stats
06. renders more rows than fit on screen (overscan)
07. shows the name and email for each mounted row
08. moves the window down when scrolling
09. restores the first rows when scrolling back to the top
10. renders the last contact when scrolled to the bottom
11. mounts overscan rows above the visible window when scrolled
12. never renders rows above the first one


