# react-utils/virtual-list

Windowed list renderer — renders only the rows visible in a scrollable container plus configurable overscan rows, keeping DOM size constant regardless of total item count.

<iframe src="examples/virtual-list/" class="example-frame" title="VirtualList example"></iframe>

```typescript
import { VirtualList } from '@yaoc1996/js-tools/react-utils'
```

---

## Component

---

### `VirtualList({ containerRef, itemCount, itemHeight, overscan?, scrollTo?, renderItem })`

Renders a virtualised list inside an externally-owned scrollable container. The component tracks the container's scroll position and dimensions, and renders only the rows that fall within the visible window plus `overscan` rows on each side.

The component itself renders a fragment: a top spacer `<div>`, the visible row elements, and a bottom spacer `<div>`. The caller is responsible for making `containerRef.current` scrollable (e.g. `overflow-y: auto` with a fixed height).

#### Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `containerRef` | `RefObject<HTMLElement \| null>` | — | The scrollable container element. Must be stable across renders. |
| `itemCount` | `number` | — | Total number of rows. |
| `itemHeight` | `number` | — | Height of each row in px. Uniform — all rows must have the same height. |
| `overscan` | `number` | `10` | Number of extra rows to render beyond the visible bounds on each side. |
| `scrollTo` | `number \| null` | — | When set, smoothly scrolls the container so that the row at this index is at the top. Re-triggers on every value change. |
| `renderItem` | `(index: number) => ReactNode` | — | Called for each visible (+ overscan) row index. Must supply a `key` prop on the returned element. |

#### Example

```tsx
const containerRef = useRef<HTMLDivElement>(null)

<div ref={containerRef} style={{ height: 400, overflowY: 'auto' }}>
  <VirtualList
    containerRef={containerRef}
    itemCount={items.length}
    itemHeight={48}
    overscan={8}
    scrollTo={focusedIndex}
    renderItem={(i) => <Row key={i} data={items[i]} />}
  />
</div>
```

#### Notes

- Row height is uniform. Variable-height rows are not supported.
- `scrollTo` uses `behavior: "smooth"`. To jump instantly, set `itemHeight` and compute the target `scrollTop` manually via `containerRef.current.scrollTop = index * itemHeight`.
- The component does not set `position`, `overflow`, or any layout styles on the container — that is the caller's responsibility.
- `containerRef` should be a stable ref (e.g. from `useRef`). Changing the ref object between renders restarts the scroll listener.

---

## Types

```typescript
interface VirtualListProps {
    containerRef: RefObject<HTMLElement | null>;
    itemCount: number;
    itemHeight: number;
    overscan?: number;
    scrollTo?: number | null;
    renderItem: (index: number) => React.ReactNode;
}
```
