Skip to content

Instantly share code, notes, and snippets.

@lukesmurray
Created March 8, 2022 12:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukesmurray/1015c6dae664a35de8ab890e601f8650 to your computer and use it in GitHub Desktop.
Save lukesmurray/1015c6dae664a35de8ab890e601f8650 to your computer and use it in GitHub Desktop.
react virtual with resize observers
import { useEffect, useMemo, useRef } from "react";
import { Options, useVirtual as useVirtualImpl } from "react-virtual";
// implementation of useVirtual that handles resizes after the element renders
// from https://codesandbox.io/s/usevirtualresizeobserver-04-11-2021-75ye2?file=/src/useVirtual.tsx
// found from this issue https://github.com/TanStack/react-virtual/issues/28
// which linked to this comment https://github.com/TanStack/react-virtual/discussions/212#discussioncomment-1587478
const defaultKeyExtractor = (index: number) => index;
export const useVirtualRO = <T extends HTMLElement>({
...options
}: Options<T>) => {
// cache of virtualItem keys to measureRef
const measureRefCacheRef = useRef<
Record<string, (el: HTMLElement | null) => void>
>({});
// cache of virtualItem keys to elements
const elCacheRef = useRef<Record<string, Element | null>>({});
// update the size associeted with a key and element
const update = (key: number | string, el: HTMLElement) => {
measureRefCacheRef.current[key](el);
};
const updateRef = useRef(update);
updateRef.current = update;
// create a resize observer for measuring elements
const roRef = useRef(
new ResizeObserver((entries) => {
entries.forEach((entry) => {
const el = entry.target;
const attribute = "data-key";
const key = el.getAttribute(attribute);
if (key === null) {
throw new Error(`Value not found, for '${attribute}' attribute`);
}
const htmlEl = el as HTMLElement;
updateRef.current(key, htmlEl);
});
})
);
// stop watching elmeents when the resize observer disconnects
useEffect(() => {
const ro = roRef.current;
return () => {
ro.disconnect();
};
}, []);
const { size, keyExtractor = defaultKeyExtractor } = options;
// create callbacks for observing elements
const cachedMeasureRefWrappers = useMemo(() => {
const makeMeasureRefWrapperForItem =
(key: string | number) => (el: HTMLElement | null) => {
if (elCacheRef.current[key]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
roRef.current.unobserve(elCacheRef.current[key]!);
}
if (el) {
// sync
updateRef.current(key, el);
// observe
roRef.current.observe(el);
}
elCacheRef.current[key] = el;
};
const refsAcc: Record<string, (el: HTMLElement | null) => void> = {};
for (let i = 0; i < size; i++) {
const key = keyExtractor(i);
refsAcc[key] = makeMeasureRefWrapperForItem(key);
}
return refsAcc;
}, [size, keyExtractor]);
const rowVirtualizer = useVirtualImpl(options);
const virtualItems = rowVirtualizer.virtualItems.map((item) => {
measureRefCacheRef.current[item.key] = item.measureRef;
return {
...item,
measureRef: cachedMeasureRefWrappers[item.key],
};
});
return { ...rowVirtualizer, virtualItems };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment