clear-virtualizer 0.1.1 → 0.1.3
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 +4 -1
- package/lib/index.cjs +53 -34
- package/lib/index.js +53 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
241
|
- Vertical scrolling only — no horizontal virtualization, no sticky columns.
|
|
238
242
|
- The internal offset tree keeps one `float64` per row (~8 MB per million rows).
|
|
239
|
-
- Server-side rendering is not covered by tests.
|
|
240
243
|
|
|
241
244
|
## License
|
|
242
245
|
|
package/lib/index.cjs
CHANGED
|
@@ -111,6 +111,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
111
111
|
const [scrollOffset, setScrollOffset] = react.useState({ value: 0 });
|
|
112
112
|
const [forcedRange, setForcedRange] = react.useState(null);
|
|
113
113
|
const scrollElementRef = react.useRef(null);
|
|
114
|
+
const [scrollElement, setScrollElement] = react.useState(null);
|
|
114
115
|
const rafRef = react.useRef(null);
|
|
115
116
|
const observersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
116
117
|
const containerObserverRef = react.useRef(null);
|
|
@@ -188,22 +189,9 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
188
189
|
},
|
|
189
190
|
[count, forcedRange, overscan]
|
|
190
191
|
);
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
const entry = observersRef.current.get(index);
|
|
194
|
-
if (entry) {
|
|
195
|
-
entry.observer.disconnect();
|
|
196
|
-
observersRef.current.delete(index);
|
|
197
|
-
}
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const existing = observersRef.current.get(index);
|
|
201
|
-
if (existing && existing.element === element)
|
|
192
|
+
const observeItem = react.useCallback((index, element) => {
|
|
193
|
+
if (typeof ResizeObserver === "undefined")
|
|
202
194
|
return;
|
|
203
|
-
if (existing) {
|
|
204
|
-
existing.observer.disconnect();
|
|
205
|
-
observersRef.current.delete(index);
|
|
206
|
-
}
|
|
207
195
|
const observer = new ResizeObserver(([entry]) => {
|
|
208
196
|
const height = entry?.borderBoxSize[0]?.blockSize;
|
|
209
197
|
if (height == null || !fenwickRef.current)
|
|
@@ -221,6 +209,27 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
221
209
|
observer.observe(element);
|
|
222
210
|
observersRef.current.set(index, { observer, element });
|
|
223
211
|
}, []);
|
|
212
|
+
const measureElement = react.useCallback(
|
|
213
|
+
(element, index) => {
|
|
214
|
+
if (!element) {
|
|
215
|
+
const entry = observersRef.current.get(index);
|
|
216
|
+
if (entry) {
|
|
217
|
+
entry.observer.disconnect();
|
|
218
|
+
observersRef.current.delete(index);
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const existing = observersRef.current.get(index);
|
|
223
|
+
if (existing && existing.element === element)
|
|
224
|
+
return;
|
|
225
|
+
if (existing) {
|
|
226
|
+
existing.observer.disconnect();
|
|
227
|
+
observersRef.current.delete(index);
|
|
228
|
+
}
|
|
229
|
+
observeItem(index, element);
|
|
230
|
+
},
|
|
231
|
+
[observeItem]
|
|
232
|
+
);
|
|
224
233
|
const getMeasureRef = react.useCallback(
|
|
225
234
|
(index) => {
|
|
226
235
|
let fn = refCacheRef.current.get(index);
|
|
@@ -350,27 +359,37 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
350
359
|
setScrollOffset({ value: el.scrollTop });
|
|
351
360
|
});
|
|
352
361
|
}, [releaseSettle, countRef]);
|
|
353
|
-
const scrollRef = react.useCallback(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
362
|
+
const scrollRef = react.useCallback((element) => {
|
|
363
|
+
scrollElementRef.current = element;
|
|
364
|
+
setScrollElement(element);
|
|
365
|
+
setScrollOffset(element ? { value: element.scrollTop } : { value: 0 });
|
|
366
|
+
}, []);
|
|
367
|
+
react.useEffect(() => {
|
|
368
|
+
const el = scrollElement;
|
|
369
|
+
const itemObservers = observersRef.current;
|
|
370
|
+
if (el) {
|
|
371
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
372
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
373
|
+
const observer = new ResizeObserver(() => {
|
|
374
|
+
setScrollOffset((prevState) => ({ ...prevState }));
|
|
375
|
+
});
|
|
376
|
+
observer.observe(el);
|
|
377
|
+
containerObserverRef.current = observer;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const [index, { element }] of Array.from(itemObservers))
|
|
381
|
+
observeItem(index, element);
|
|
382
|
+
return () => {
|
|
383
|
+
if (rafRef.current !== null)
|
|
384
|
+
cancelAnimationFrame(rafRef.current);
|
|
385
|
+
if (tickRafRef.current !== null)
|
|
386
|
+
cancelAnimationFrame(tickRafRef.current);
|
|
387
|
+
el?.removeEventListener("scroll", handleScroll);
|
|
357
388
|
containerObserverRef.current?.disconnect();
|
|
358
389
|
containerObserverRef.current = null;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
363
|
-
const observer = new ResizeObserver(() => {
|
|
364
|
-
setScrollOffset((prevState) => ({ ...prevState }));
|
|
365
|
-
});
|
|
366
|
-
observer.observe(element);
|
|
367
|
-
containerObserverRef.current = observer;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
setScrollOffset(element ? { value: element?.scrollTop } : { value: 0 });
|
|
371
|
-
},
|
|
372
|
-
[handleScroll]
|
|
373
|
-
);
|
|
390
|
+
itemObservers.forEach((entry) => entry.observer.disconnect());
|
|
391
|
+
};
|
|
392
|
+
}, [scrollElement, handleScroll, observeItem]);
|
|
374
393
|
const { virtualItems, scrollHeight } = react.useMemo(
|
|
375
394
|
() => computeItems(scrollOffset.value),
|
|
376
395
|
[computeItems, scrollOffset]
|
package/lib/index.js
CHANGED
|
@@ -109,6 +109,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
109
109
|
const [scrollOffset, setScrollOffset] = useState({ value: 0 });
|
|
110
110
|
const [forcedRange, setForcedRange] = useState(null);
|
|
111
111
|
const scrollElementRef = useRef(null);
|
|
112
|
+
const [scrollElement, setScrollElement] = useState(null);
|
|
112
113
|
const rafRef = useRef(null);
|
|
113
114
|
const observersRef = useRef(/* @__PURE__ */ new Map());
|
|
114
115
|
const containerObserverRef = useRef(null);
|
|
@@ -186,22 +187,9 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
186
187
|
},
|
|
187
188
|
[count, forcedRange, overscan]
|
|
188
189
|
);
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
const entry = observersRef.current.get(index);
|
|
192
|
-
if (entry) {
|
|
193
|
-
entry.observer.disconnect();
|
|
194
|
-
observersRef.current.delete(index);
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const existing = observersRef.current.get(index);
|
|
199
|
-
if (existing && existing.element === element)
|
|
190
|
+
const observeItem = useCallback((index, element) => {
|
|
191
|
+
if (typeof ResizeObserver === "undefined")
|
|
200
192
|
return;
|
|
201
|
-
if (existing) {
|
|
202
|
-
existing.observer.disconnect();
|
|
203
|
-
observersRef.current.delete(index);
|
|
204
|
-
}
|
|
205
193
|
const observer = new ResizeObserver(([entry]) => {
|
|
206
194
|
const height = entry?.borderBoxSize[0]?.blockSize;
|
|
207
195
|
if (height == null || !fenwickRef.current)
|
|
@@ -219,6 +207,27 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
219
207
|
observer.observe(element);
|
|
220
208
|
observersRef.current.set(index, { observer, element });
|
|
221
209
|
}, []);
|
|
210
|
+
const measureElement = useCallback(
|
|
211
|
+
(element, index) => {
|
|
212
|
+
if (!element) {
|
|
213
|
+
const entry = observersRef.current.get(index);
|
|
214
|
+
if (entry) {
|
|
215
|
+
entry.observer.disconnect();
|
|
216
|
+
observersRef.current.delete(index);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const existing = observersRef.current.get(index);
|
|
221
|
+
if (existing && existing.element === element)
|
|
222
|
+
return;
|
|
223
|
+
if (existing) {
|
|
224
|
+
existing.observer.disconnect();
|
|
225
|
+
observersRef.current.delete(index);
|
|
226
|
+
}
|
|
227
|
+
observeItem(index, element);
|
|
228
|
+
},
|
|
229
|
+
[observeItem]
|
|
230
|
+
);
|
|
222
231
|
const getMeasureRef = useCallback(
|
|
223
232
|
(index) => {
|
|
224
233
|
let fn = refCacheRef.current.get(index);
|
|
@@ -348,27 +357,37 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
348
357
|
setScrollOffset({ value: el.scrollTop });
|
|
349
358
|
});
|
|
350
359
|
}, [releaseSettle, countRef]);
|
|
351
|
-
const scrollRef = useCallback(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
const scrollRef = useCallback((element) => {
|
|
361
|
+
scrollElementRef.current = element;
|
|
362
|
+
setScrollElement(element);
|
|
363
|
+
setScrollOffset(element ? { value: element.scrollTop } : { value: 0 });
|
|
364
|
+
}, []);
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
const el = scrollElement;
|
|
367
|
+
const itemObservers = observersRef.current;
|
|
368
|
+
if (el) {
|
|
369
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
370
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
371
|
+
const observer = new ResizeObserver(() => {
|
|
372
|
+
setScrollOffset((prevState) => ({ ...prevState }));
|
|
373
|
+
});
|
|
374
|
+
observer.observe(el);
|
|
375
|
+
containerObserverRef.current = observer;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const [index, { element }] of Array.from(itemObservers))
|
|
379
|
+
observeItem(index, element);
|
|
380
|
+
return () => {
|
|
381
|
+
if (rafRef.current !== null)
|
|
382
|
+
cancelAnimationFrame(rafRef.current);
|
|
383
|
+
if (tickRafRef.current !== null)
|
|
384
|
+
cancelAnimationFrame(tickRafRef.current);
|
|
385
|
+
el?.removeEventListener("scroll", handleScroll);
|
|
355
386
|
containerObserverRef.current?.disconnect();
|
|
356
387
|
containerObserverRef.current = null;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
361
|
-
const observer = new ResizeObserver(() => {
|
|
362
|
-
setScrollOffset((prevState) => ({ ...prevState }));
|
|
363
|
-
});
|
|
364
|
-
observer.observe(element);
|
|
365
|
-
containerObserverRef.current = observer;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
setScrollOffset(element ? { value: element?.scrollTop } : { value: 0 });
|
|
369
|
-
},
|
|
370
|
-
[handleScroll]
|
|
371
|
-
);
|
|
388
|
+
itemObservers.forEach((entry) => entry.observer.disconnect());
|
|
389
|
+
};
|
|
390
|
+
}, [scrollElement, handleScroll, observeItem]);
|
|
372
391
|
const { virtualItems, scrollHeight } = useMemo(
|
|
373
392
|
() => computeItems(scrollOffset.value),
|
|
374
393
|
[computeItems, scrollOffset]
|
package/package.json
CHANGED