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 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 measureElement = react.useCallback((element, index) => {
192
- if (!element) {
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
- (element) => {
355
- if (scrollElementRef.current)
356
- scrollElementRef.current.removeEventListener("scroll", handleScroll);
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
- scrollElementRef.current = element;
360
- if (element) {
361
- element.addEventListener("scroll", handleScroll, { passive: true });
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 measureElement = useCallback((element, index) => {
190
- if (!element) {
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
- (element) => {
353
- if (scrollElementRef.current)
354
- scrollElementRef.current.removeEventListener("scroll", handleScroll);
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
- scrollElementRef.current = element;
358
- if (element) {
359
- element.addEventListener("scroll", handleScroll, { passive: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-virtualizer",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",