Created
March 8, 2022 12:58
-
-
Save lukesmurray/1015c6dae664a35de8ab890e601f8650 to your computer and use it in GitHub Desktop.
react virtual with resize observers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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