@tanstack/react-virtual
Version:
Headless UI for virtualizing scrollable elements in React
1 lines • 12 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../../src/index.tsx"],"sourcesContent":["import * as React from 'react'\nimport { flushSync } from 'react-dom'\nimport {\n Virtualizer,\n elementScroll,\n observeElementOffset,\n observeElementRect,\n observeWindowOffset,\n observeWindowRect,\n windowScroll,\n} from '@tanstack/virtual-core'\nimport type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core'\n\nexport * from '@tanstack/virtual-core'\n\nconst useIsomorphicLayoutEffect =\n typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect\n\nexport type ReactVirtualizer<\n TScrollElement extends Element | Window,\n TItemElement extends Element,\n> = Virtualizer<TScrollElement, TItemElement> & {\n /**\n * Ref callback for the inner size container element. Only meaningful when\n * `directDomUpdates: true` — the virtualizer writes the container's\n * main-axis size (`height` or `width`) directly to skip React re-renders.\n */\n containerRef: (node: HTMLElement | null) => void\n}\n\nexport type ReactVirtualizerOptions<\n TScrollElement extends Element | Window,\n TItemElement extends Element,\n> = VirtualizerOptions<TScrollElement, TItemElement> & {\n useFlushSync?: boolean\n /**\n * Skip React re-renders for scroll-only updates. The virtualizer writes\n * item positions (`top`/`left`) and the container size (`height`/`width`)\n * directly to the DOM, and only re-renders when the visible index range\n * or `isScrolling` changes.\n *\n * Requirements when enabled:\n * - Item elements must be `position: absolute`; in `'transform'` mode they\n * must also be anchored with `top: 0` / `left: 0`.\n * - Item elements must NOT set the main-axis position in their style — the\n * virtualizer owns `top` / `left` in `'position'` mode and `transform` in\n * `'transform'` mode.\n * - The inner size container must receive `virtualizer.containerRef` and\n * must NOT set `height` / `width` in its style.\n * - For multi-lane layouts (grids / masonry), the cross-axis position\n * (e.g. `left: ${(item.lane * 100) / lanes}%`) is stable per item and\n * must still be set in your JSX — only the main axis is automated.\n *\n * This flag is intended to be set once at mount. Toggling it (or\n * `directDomUpdatesMode`) at runtime can leave stale inline styles on\n * items and the container.\n */\n directDomUpdates?: boolean\n /**\n * How `directDomUpdates` positions item elements.\n * - `'transform'` (default): writes `transform: translate3d(...)`.\n * Promotes items to their own compositor layer — usually smoother on long\n * lists, but creates a stacking context and can interfere with\n * `position: fixed` descendants. Item elements must still be anchored with\n * `position: absolute`, `top: 0`, and `left: 0`.\n * - `'position'`: writes `top` / `left`. Item elements must be\n * `position: absolute`.\n */\n directDomUpdatesMode?: 'position' | 'transform'\n}\n\nfunction useVirtualizerBase<\n TScrollElement extends Element | Window,\n TItemElement extends Element,\n>({\n useFlushSync = true,\n directDomUpdates = false,\n directDomUpdatesMode = 'transform',\n ...options\n}: ReactVirtualizerOptions<TScrollElement, TItemElement>): ReactVirtualizer<\n TScrollElement,\n TItemElement\n> {\n const rerender = React.useReducer((x: number) => x + 1, 0)[1]\n\n // Mutable across renders so the onChange closure captured by setOptions\n // always reads the latest values without us having to re-create it.\n const directRef = React.useRef({\n enabled: directDomUpdates,\n mode: directDomUpdatesMode,\n container: null as HTMLElement | null,\n lastSize: null as number | null,\n // Keyed by the element itself so a remounted node (same key, new DOM\n // node — e.g. when `enabled` is toggled off then on) is treated as fresh\n // and gets its style written.\n lastPositions: new WeakMap<HTMLElement, number>(),\n prevRange: null as {\n startIndex: number\n endIndex: number\n isScrolling: boolean\n } | null,\n })\n directRef.current.enabled = directDomUpdates\n directRef.current.mode = directDomUpdatesMode\n\n // Writes container size + item positions to the DOM. Idempotent — guarded\n // by lastSize / lastPositions. Called from onChange (covers scroll-driven\n // updates) and from a layout effect (covers post-render commits when refs\n // have just registered new items in elementsCache).\n const applyDirectStyles = (\n instance: Virtualizer<TScrollElement, TItemElement>,\n ) => {\n const state = directRef.current\n if (!state.enabled) return\n\n const totalSize = instance.getTotalSize()\n if (state.container && totalSize !== state.lastSize) {\n state.lastSize = totalSize\n const sizeAxis = instance.options.horizontal ? 'width' : 'height'\n state.container.style[sizeAxis] = `${totalSize}px`\n }\n\n const horizontal = !!instance.options.horizontal\n const useTransform = state.mode === 'transform'\n const posAxis = horizontal ? 'left' : 'top'\n const scrollMargin = instance.options.scrollMargin\n const items = instance.getVirtualItems()\n for (const item of items) {\n const next = item.start - scrollMargin\n const el = instance.elementsCache.get(item.key) as HTMLElement | undefined\n if (!el) continue\n if (state.lastPositions.get(el) === next) continue\n state.lastPositions.set(el, next)\n if (useTransform) {\n el.style.transform = horizontal\n ? `translate3d(${next}px, 0, 0)`\n : `translate3d(0, ${next}px, 0)`\n } else {\n el.style[posAxis] = `${next}px`\n }\n }\n }\n\n const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> = {\n ...options,\n onChange: (instance, sync) => {\n const state = directRef.current\n let shouldRerender = true\n\n if (state.enabled) {\n applyDirectStyles(instance)\n\n // Only re-render on range / isScrolling changes\n const range = instance.range\n const prev = state.prevRange\n shouldRerender =\n !prev ||\n prev.isScrolling !== instance.isScrolling ||\n prev.startIndex !== range?.startIndex ||\n prev.endIndex !== range?.endIndex\n if (shouldRerender) {\n state.prevRange = range\n ? {\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n isScrolling: instance.isScrolling,\n }\n : null\n }\n }\n\n if (shouldRerender) {\n if (useFlushSync && sync) {\n flushSync(rerender)\n } else {\n rerender()\n }\n }\n\n options.onChange?.(instance, sync)\n },\n }\n\n const [instance] = React.useState(() => {\n const v = new Virtualizer<TScrollElement, TItemElement>(resolvedOptions)\n return Object.assign(v, {\n containerRef: (node: HTMLElement | null) => {\n const state = directRef.current\n state.container = node\n state.lastSize = null\n if (node && state.enabled) {\n const total = v.getTotalSize()\n state.lastSize = total\n const axis = v.options.horizontal ? 'width' : 'height'\n node.style[axis] = `${total}px`\n }\n },\n })\n })\n\n instance.setOptions(resolvedOptions)\n\n useIsomorphicLayoutEffect(() => {\n return instance._didMount()\n }, [])\n\n useIsomorphicLayoutEffect(() => {\n return instance._willUpdate()\n })\n\n // After every render commit, newly mounted item refs have registered in\n // elementsCache; write their positions to the DOM so the user doesn't see\n // them at (0, 0) until the next onChange.\n useIsomorphicLayoutEffect(() => {\n applyDirectStyles(instance)\n })\n\n return instance\n}\n\nexport function useVirtualizer<\n TScrollElement extends Element,\n TItemElement extends Element,\n>(\n options: PartialKeys<\n ReactVirtualizerOptions<TScrollElement, TItemElement>,\n 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'\n >,\n): ReactVirtualizer<TScrollElement, TItemElement> {\n return useVirtualizerBase<TScrollElement, TItemElement>({\n observeElementRect: observeElementRect,\n observeElementOffset: observeElementOffset,\n scrollToFn: elementScroll,\n ...options,\n })\n}\n\nexport function useWindowVirtualizer<TItemElement extends Element>(\n options: PartialKeys<\n ReactVirtualizerOptions<Window, TItemElement>,\n | 'getScrollElement'\n | 'observeElementRect'\n | 'observeElementOffset'\n | 'scrollToFn'\n >,\n): ReactVirtualizer<Window, TItemElement> {\n return useVirtualizerBase<Window, TItemElement>({\n getScrollElement: () => (typeof document !== 'undefined' ? window : null),\n observeElementRect: observeWindowRect,\n observeElementOffset: observeWindowOffset,\n scrollToFn: windowScroll,\n initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0),\n ...options,\n })\n}\n"],"names":["React","instance","flushSync","Virtualizer","observeElementRect","observeElementOffset","elementScroll","observeWindowRect","observeWindowOffset","windowScroll"],"mappings":"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"}