clear-virtualizer 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -24
- package/lib/index.cjs +17 -0
- package/lib/index.js +17 -0
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/clear-virtualizer)
|
|
2
|
+
|
|
1
3
|
# clear-virtualizer
|
|
2
4
|
|
|
3
5
|
One hook for vertical virtualized lists and tables with dynamic row heights. Rows are measured automatically via `ResizeObserver` — no manual measuring, no fixed heights. Works for an unlimited number of rows.
|
|
@@ -18,25 +20,25 @@ import { useVirtualizer, type Options, type ScrollAlign, type VirtualItem } from
|
|
|
18
20
|
|
|
19
21
|
```ts
|
|
20
22
|
const { virtualItems, scrollHeight, scrollRef, getMeasureRef, scrollToIndex } = useVirtualizer({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
count: 100000,
|
|
24
|
+
estimateSize: () => 44,
|
|
25
|
+
overscan: 10,
|
|
24
26
|
});
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
| Option | Type | Description
|
|
28
|
-
| --------------
|
|
29
|
-
| `count` | `number` | Total number of rows
|
|
29
|
+
| Option | Type | Description |
|
|
30
|
+
| -------------- | ------------------------------------------ | ---------------------------------------------------------- |
|
|
31
|
+
| `count` | `number` | Total number of rows |
|
|
30
32
|
| `estimateSize` | `((index: number) => number) \| undefined` | Approximate row height in px, used until a row is measured |
|
|
31
|
-
| `overscan` | `number \| undefined` | Extra rows rendered above/below the viewport (default: 3)
|
|
33
|
+
| `overscan` | `number \| undefined` | Extra rows rendered above/below the viewport (default: 3) |
|
|
32
34
|
|
|
33
|
-
| Return value | Type
|
|
34
|
-
| --------------- |
|
|
35
|
-
| `virtualItems` | `VirtualItem[]`
|
|
36
|
-
| `scrollHeight` | `number`
|
|
37
|
-
| `scrollRef` | `(el: HTMLElement \| null) => void`
|
|
38
|
-
| `getMeasureRef` | `(index: number) => (el: HTMLElement \| null) => void`
|
|
39
|
-
| `scrollToIndex` | `(index: number, options?: { align?: ScrollAlign }) => void` | Scroll to a row
|
|
35
|
+
| Return value | Type | Description |
|
|
36
|
+
| --------------- | ------------------------------------------------------------ | -------------------------------------------------- |
|
|
37
|
+
| `virtualItems` | `VirtualItem[]` | Rows to render: `{ index, start, size, end }` (px) |
|
|
38
|
+
| `scrollHeight` | `number` | Total scrollable height (px) |
|
|
39
|
+
| `scrollRef` | `(el: HTMLElement \| null) => void` | Callback ref — attach to the scroll container |
|
|
40
|
+
| `getMeasureRef` | `(index: number) => (el: HTMLElement \| null) => void` | Attach the returned ref to each row for measuring |
|
|
41
|
+
| `scrollToIndex` | `(index: number, options?: { align?: ScrollAlign }) => void` | Scroll to a row |
|
|
40
42
|
|
|
41
43
|
Do not set a fixed `height` on rows — let them size naturally so `getMeasureRef` measures the real height. Position rows with `transform: translateY(...)`.
|
|
42
44
|
|
|
@@ -131,9 +133,7 @@ const columns = [
|
|
|
131
133
|
columnHelper.display({
|
|
132
134
|
id: 'expand',
|
|
133
135
|
size: 40,
|
|
134
|
-
cell: ({ row }) => (
|
|
135
|
-
<button onClick={() => row.toggleExpanded()}>{row.getIsExpanded() ? '▼' : '▶'}</button>
|
|
136
|
-
),
|
|
136
|
+
cell: ({ row }) => <button onClick={() => row.toggleExpanded()}>{row.getIsExpanded() ? '▼' : '▶'}</button>,
|
|
137
137
|
}),
|
|
138
138
|
columnHelper.accessor('id', { header: 'ID', size: 60 }),
|
|
139
139
|
columnHelper.accessor('name', { header: 'Name', size: 150 }),
|
|
@@ -220,11 +220,11 @@ scrollToIndex(34567, { align: 'center' });
|
|
|
220
220
|
scrollToIndex(34567, { align: 'end' });
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
| `align` | Positions the row...
|
|
224
|
-
| -------- |
|
|
223
|
+
| `align` | Positions the row... |
|
|
224
|
+
| -------- | ------------------------------------ |
|
|
225
225
|
| `start` | At the top of the viewport (default) |
|
|
226
|
-
| `center` | In the middle of the viewport
|
|
227
|
-
| `end` | At the bottom of the viewport
|
|
226
|
+
| `center` | In the middle of the viewport |
|
|
227
|
+
| `end` | At the bottom of the viewport |
|
|
228
228
|
|
|
229
229
|
If the row is far away and its neighbors were never measured, the hook first jumps to the estimated position, measures the rows around the target, and corrects the position. A manual scroll during that correction cancels it and hands control back to the user.
|
|
230
230
|
|
|
@@ -232,11 +232,14 @@ If the row is far away and its neighbors were never measured, the hook first jum
|
|
|
232
232
|
|
|
233
233
|
`estimateSize` is only a starting guess until real measurements arrive. The closer it is to reality, the less rows visually shift on fast scroll through unmeasured regions: underestimated rows overlap until measured, overestimated ones leave temporary gaps. Pass a value near your average row height.
|
|
234
234
|
|
|
235
|
+
## SSR
|
|
236
|
+
|
|
237
|
+
Server-side rendering is supported: on the server the hook renders an empty container (`virtualItems: []`, `scrollHeight: 0`) without touching browser APIs. Rows appear after hydration once the scroll container is attached and `ResizeObserver` reports real heights.
|
|
238
|
+
|
|
235
239
|
## Limitations
|
|
236
240
|
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
- Server-side rendering is not covered by tests.
|
|
241
|
+
- Vertical scrolling only — no horizontal virtualization, no sticky columns.
|
|
242
|
+
- The internal offset tree keeps one `float64` per row (~8 MB per million rows).
|
|
240
243
|
|
|
241
244
|
## License
|
|
242
245
|
|
package/lib/index.cjs
CHANGED
|
@@ -204,6 +204,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
204
204
|
existing.observer.disconnect();
|
|
205
205
|
observersRef.current.delete(index);
|
|
206
206
|
}
|
|
207
|
+
if (typeof ResizeObserver === "undefined")
|
|
208
|
+
return;
|
|
207
209
|
const observer = new ResizeObserver(([entry]) => {
|
|
208
210
|
const height = entry?.borderBoxSize[0]?.blockSize;
|
|
209
211
|
if (height == null || !fenwickRef.current)
|
|
@@ -371,6 +373,21 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
371
373
|
},
|
|
372
374
|
[handleScroll]
|
|
373
375
|
);
|
|
376
|
+
react.useEffect(
|
|
377
|
+
() => () => {
|
|
378
|
+
if (rafRef.current !== null)
|
|
379
|
+
cancelAnimationFrame(rafRef.current);
|
|
380
|
+
if (tickRafRef.current !== null)
|
|
381
|
+
cancelAnimationFrame(tickRafRef.current);
|
|
382
|
+
observersRef.current.forEach((entry) => entry.observer.disconnect());
|
|
383
|
+
observersRef.current.clear();
|
|
384
|
+
containerObserverRef.current?.disconnect();
|
|
385
|
+
containerObserverRef.current = null;
|
|
386
|
+
scrollElementRef.current?.removeEventListener("scroll", handleScroll);
|
|
387
|
+
refCacheRef.current.clear();
|
|
388
|
+
},
|
|
389
|
+
[handleScroll]
|
|
390
|
+
);
|
|
374
391
|
const { virtualItems, scrollHeight } = react.useMemo(
|
|
375
392
|
() => computeItems(scrollOffset.value),
|
|
376
393
|
[computeItems, scrollOffset]
|
package/lib/index.js
CHANGED
|
@@ -202,6 +202,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
202
202
|
existing.observer.disconnect();
|
|
203
203
|
observersRef.current.delete(index);
|
|
204
204
|
}
|
|
205
|
+
if (typeof ResizeObserver === "undefined")
|
|
206
|
+
return;
|
|
205
207
|
const observer = new ResizeObserver(([entry]) => {
|
|
206
208
|
const height = entry?.borderBoxSize[0]?.blockSize;
|
|
207
209
|
if (height == null || !fenwickRef.current)
|
|
@@ -369,6 +371,21 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
369
371
|
},
|
|
370
372
|
[handleScroll]
|
|
371
373
|
);
|
|
374
|
+
useEffect(
|
|
375
|
+
() => () => {
|
|
376
|
+
if (rafRef.current !== null)
|
|
377
|
+
cancelAnimationFrame(rafRef.current);
|
|
378
|
+
if (tickRafRef.current !== null)
|
|
379
|
+
cancelAnimationFrame(tickRafRef.current);
|
|
380
|
+
observersRef.current.forEach((entry) => entry.observer.disconnect());
|
|
381
|
+
observersRef.current.clear();
|
|
382
|
+
containerObserverRef.current?.disconnect();
|
|
383
|
+
containerObserverRef.current = null;
|
|
384
|
+
scrollElementRef.current?.removeEventListener("scroll", handleScroll);
|
|
385
|
+
refCacheRef.current.clear();
|
|
386
|
+
},
|
|
387
|
+
[handleScroll]
|
|
388
|
+
);
|
|
372
389
|
const { virtualItems, scrollHeight } = useMemo(
|
|
373
390
|
() => computeItems(scrollOffset.value),
|
|
374
391
|
[computeItems, scrollOffset]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clear-virtualizer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A lightweight React hook for virtualized lists and tables with dynamic row heights, measured automatically",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.cjs",
|
|
@@ -85,7 +85,6 @@
|
|
|
85
85
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
86
86
|
"eslint-plugin-react-refresh": "^0.4.3",
|
|
87
87
|
"jsdom": "^30.0.1",
|
|
88
|
-
"lorem-ipsum": "^2.0.8",
|
|
89
88
|
"react": "^18.2.0",
|
|
90
89
|
"react-dom": "^18.2.0",
|
|
91
90
|
"typescript": "^5.0.2",
|