clear-virtualizer 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/lib/index.cjs +59 -46
  2. package/lib/index.js +59 -46
  3. package/package.json +1 -1
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);
@@ -128,7 +129,18 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
128
129
  if (fenwickRef.current === null || !fenwickRef.current.total())
129
130
  fenwickRef.current = getFenwickTree(count, estimateSize);
130
131
  if (prevCountRef.current !== count) {
131
- fenwickRef.current = getFenwickTree(count, estimateSize);
132
+ const prevTree = fenwickRef.current;
133
+ const prevCount = prevCountRef.current;
134
+ const nextTree = getFenwickTree(count, estimateSize);
135
+ if (prevTree && prevCount > 0) {
136
+ const keep = Math.min(prevCount, count);
137
+ for (let i = 0; i < keep; i++) {
138
+ const delta = prevTree.prefixSum(i + 1) - prevTree.prefixSum(i) - (nextTree.prefixSum(i + 1) - nextTree.prefixSum(i));
139
+ if (delta !== 0)
140
+ nextTree.update(i, delta);
141
+ }
142
+ }
143
+ fenwickRef.current = nextTree;
132
144
  prevCountRef.current = count;
133
145
  }
134
146
  const computeItems = react.useCallback(
@@ -188,22 +200,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
188
200
  },
189
201
  [count, forcedRange, overscan]
190
202
  );
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)
202
- return;
203
- if (existing) {
204
- existing.observer.disconnect();
205
- observersRef.current.delete(index);
206
- }
203
+ const observeItem = react.useCallback((index, element) => {
207
204
  if (typeof ResizeObserver === "undefined")
208
205
  return;
209
206
  const observer = new ResizeObserver(([entry]) => {
@@ -223,6 +220,27 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
223
220
  observer.observe(element);
224
221
  observersRef.current.set(index, { observer, element });
225
222
  }, []);
223
+ const measureElement = react.useCallback(
224
+ (element, index) => {
225
+ if (!element) {
226
+ const entry = observersRef.current.get(index);
227
+ if (entry) {
228
+ entry.observer.disconnect();
229
+ observersRef.current.delete(index);
230
+ }
231
+ return;
232
+ }
233
+ const existing = observersRef.current.get(index);
234
+ if (existing && existing.element === element)
235
+ return;
236
+ if (existing) {
237
+ existing.observer.disconnect();
238
+ observersRef.current.delete(index);
239
+ }
240
+ observeItem(index, element);
241
+ },
242
+ [observeItem]
243
+ );
226
244
  const getMeasureRef = react.useCallback(
227
245
  (index) => {
228
246
  let fn = refCacheRef.current.get(index);
@@ -352,42 +370,37 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
352
370
  setScrollOffset({ value: el.scrollTop });
353
371
  });
354
372
  }, [releaseSettle, countRef]);
355
- const scrollRef = react.useCallback(
356
- (element) => {
357
- if (scrollElementRef.current)
358
- scrollElementRef.current.removeEventListener("scroll", handleScroll);
359
- containerObserverRef.current?.disconnect();
360
- containerObserverRef.current = null;
361
- scrollElementRef.current = element;
362
- if (element) {
363
- element.addEventListener("scroll", handleScroll, { passive: true });
364
- if (typeof ResizeObserver !== "undefined") {
365
- const observer = new ResizeObserver(() => {
366
- setScrollOffset((prevState) => ({ ...prevState }));
367
- });
368
- observer.observe(element);
369
- containerObserverRef.current = observer;
370
- }
373
+ const scrollRef = react.useCallback((element) => {
374
+ scrollElementRef.current = element;
375
+ setScrollElement(element);
376
+ setScrollOffset(element ? { value: element.scrollTop } : { value: 0 });
377
+ }, []);
378
+ react.useEffect(() => {
379
+ const el = scrollElement;
380
+ const itemObservers = observersRef.current;
381
+ if (el) {
382
+ el.addEventListener("scroll", handleScroll, { passive: true });
383
+ if (typeof ResizeObserver !== "undefined") {
384
+ const observer = new ResizeObserver(() => {
385
+ setScrollOffset((prevState) => ({ ...prevState }));
386
+ });
387
+ observer.observe(el);
388
+ containerObserverRef.current = observer;
371
389
  }
372
- setScrollOffset(element ? { value: element?.scrollTop } : { value: 0 });
373
- },
374
- [handleScroll]
375
- );
376
- react.useEffect(
377
- () => () => {
390
+ }
391
+ for (const [index, { element }] of Array.from(itemObservers))
392
+ observeItem(index, element);
393
+ return () => {
378
394
  if (rafRef.current !== null)
379
395
  cancelAnimationFrame(rafRef.current);
380
396
  if (tickRafRef.current !== null)
381
397
  cancelAnimationFrame(tickRafRef.current);
382
- observersRef.current.forEach((entry) => entry.observer.disconnect());
383
- observersRef.current.clear();
398
+ el?.removeEventListener("scroll", handleScroll);
384
399
  containerObserverRef.current?.disconnect();
385
400
  containerObserverRef.current = null;
386
- scrollElementRef.current?.removeEventListener("scroll", handleScroll);
387
- refCacheRef.current.clear();
388
- },
389
- [handleScroll]
390
- );
401
+ itemObservers.forEach((entry) => entry.observer.disconnect());
402
+ };
403
+ }, [scrollElement, handleScroll, observeItem]);
391
404
  const { virtualItems, scrollHeight } = react.useMemo(
392
405
  () => computeItems(scrollOffset.value),
393
406
  [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);
@@ -126,7 +127,18 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
126
127
  if (fenwickRef.current === null || !fenwickRef.current.total())
127
128
  fenwickRef.current = getFenwickTree(count, estimateSize);
128
129
  if (prevCountRef.current !== count) {
129
- fenwickRef.current = getFenwickTree(count, estimateSize);
130
+ const prevTree = fenwickRef.current;
131
+ const prevCount = prevCountRef.current;
132
+ const nextTree = getFenwickTree(count, estimateSize);
133
+ if (prevTree && prevCount > 0) {
134
+ const keep = Math.min(prevCount, count);
135
+ for (let i = 0; i < keep; i++) {
136
+ const delta = prevTree.prefixSum(i + 1) - prevTree.prefixSum(i) - (nextTree.prefixSum(i + 1) - nextTree.prefixSum(i));
137
+ if (delta !== 0)
138
+ nextTree.update(i, delta);
139
+ }
140
+ }
141
+ fenwickRef.current = nextTree;
130
142
  prevCountRef.current = count;
131
143
  }
132
144
  const computeItems = useCallback(
@@ -186,22 +198,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
186
198
  },
187
199
  [count, forcedRange, overscan]
188
200
  );
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)
200
- return;
201
- if (existing) {
202
- existing.observer.disconnect();
203
- observersRef.current.delete(index);
204
- }
201
+ const observeItem = useCallback((index, element) => {
205
202
  if (typeof ResizeObserver === "undefined")
206
203
  return;
207
204
  const observer = new ResizeObserver(([entry]) => {
@@ -221,6 +218,27 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
221
218
  observer.observe(element);
222
219
  observersRef.current.set(index, { observer, element });
223
220
  }, []);
221
+ const measureElement = useCallback(
222
+ (element, index) => {
223
+ if (!element) {
224
+ const entry = observersRef.current.get(index);
225
+ if (entry) {
226
+ entry.observer.disconnect();
227
+ observersRef.current.delete(index);
228
+ }
229
+ return;
230
+ }
231
+ const existing = observersRef.current.get(index);
232
+ if (existing && existing.element === element)
233
+ return;
234
+ if (existing) {
235
+ existing.observer.disconnect();
236
+ observersRef.current.delete(index);
237
+ }
238
+ observeItem(index, element);
239
+ },
240
+ [observeItem]
241
+ );
224
242
  const getMeasureRef = useCallback(
225
243
  (index) => {
226
244
  let fn = refCacheRef.current.get(index);
@@ -350,42 +368,37 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
350
368
  setScrollOffset({ value: el.scrollTop });
351
369
  });
352
370
  }, [releaseSettle, countRef]);
353
- const scrollRef = useCallback(
354
- (element) => {
355
- if (scrollElementRef.current)
356
- scrollElementRef.current.removeEventListener("scroll", handleScroll);
357
- containerObserverRef.current?.disconnect();
358
- 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
- }
371
+ const scrollRef = useCallback((element) => {
372
+ scrollElementRef.current = element;
373
+ setScrollElement(element);
374
+ setScrollOffset(element ? { value: element.scrollTop } : { value: 0 });
375
+ }, []);
376
+ useEffect(() => {
377
+ const el = scrollElement;
378
+ const itemObservers = observersRef.current;
379
+ if (el) {
380
+ el.addEventListener("scroll", handleScroll, { passive: true });
381
+ if (typeof ResizeObserver !== "undefined") {
382
+ const observer = new ResizeObserver(() => {
383
+ setScrollOffset((prevState) => ({ ...prevState }));
384
+ });
385
+ observer.observe(el);
386
+ containerObserverRef.current = observer;
369
387
  }
370
- setScrollOffset(element ? { value: element?.scrollTop } : { value: 0 });
371
- },
372
- [handleScroll]
373
- );
374
- useEffect(
375
- () => () => {
388
+ }
389
+ for (const [index, { element }] of Array.from(itemObservers))
390
+ observeItem(index, element);
391
+ return () => {
376
392
  if (rafRef.current !== null)
377
393
  cancelAnimationFrame(rafRef.current);
378
394
  if (tickRafRef.current !== null)
379
395
  cancelAnimationFrame(tickRafRef.current);
380
- observersRef.current.forEach((entry) => entry.observer.disconnect());
381
- observersRef.current.clear();
396
+ el?.removeEventListener("scroll", handleScroll);
382
397
  containerObserverRef.current?.disconnect();
383
398
  containerObserverRef.current = null;
384
- scrollElementRef.current?.removeEventListener("scroll", handleScroll);
385
- refCacheRef.current.clear();
386
- },
387
- [handleScroll]
388
- );
399
+ itemObservers.forEach((entry) => entry.observer.disconnect());
400
+ };
401
+ }, [scrollElement, handleScroll, observeItem]);
389
402
  const { virtualItems, scrollHeight } = useMemo(
390
403
  () => computeItems(scrollOffset.value),
391
404
  [computeItems, scrollOffset]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-virtualizer",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",