forked from TanStack/virtual
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.tsx
More file actions
255 lines (233 loc) · 8.59 KB
/
Copy pathindex.tsx
File metadata and controls
255 lines (233 loc) · 8.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import * as React from 'react'
import { flushSync } from 'react-dom'
import {
Virtualizer,
elementScroll,
observeElementOffset,
observeElementRect,
observeWindowOffset,
observeWindowRect,
windowScroll,
} from '@tanstack/virtual-core'
import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core'
export * from '@tanstack/virtual-core'
const useIsomorphicLayoutEffect =
typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect
export type ReactVirtualizer<
TScrollElement extends Element | Window,
TItemElement extends Element,
> = Virtualizer<TScrollElement, TItemElement> & {
/**
* Ref callback for the inner size container element. Only meaningful when
* `directDomUpdates: true` — the virtualizer writes the container's
* main-axis size (`height` or `width`) directly to skip React re-renders.
*/
containerRef: (node: HTMLElement | null) => void
}
export type ReactVirtualizerOptions<
TScrollElement extends Element | Window,
TItemElement extends Element,
> = VirtualizerOptions<TScrollElement, TItemElement> & {
useFlushSync?: boolean
/**
* Skip React re-renders for scroll-only updates. The virtualizer writes
* item positions (`top`/`left`) and the container size (`height`/`width`)
* directly to the DOM, and only re-renders when the visible index range
* or `isScrolling` changes.
*
* Requirements when enabled:
* - Item elements must be `position: absolute`; in `'transform'` mode they
* must also be anchored with `top: 0` / `left: 0`.
* - Item elements must NOT set the main-axis position in their style — the
* virtualizer owns `top` / `left` in `'position'` mode and `transform` in
* `'transform'` mode.
* - The inner size container must receive `virtualizer.containerRef` and
* must NOT set `height` / `width` in its style.
* - For multi-lane layouts (grids / masonry), the cross-axis position
* (e.g. `left: ${(item.lane * 100) / lanes}%`) is stable per item and
* must still be set in your JSX — only the main axis is automated.
*
* This flag is intended to be set once at mount. Toggling it (or
* `directDomUpdatesMode`) at runtime can leave stale inline styles on
* items and the container.
*/
directDomUpdates?: boolean
/**
* How `directDomUpdates` positions item elements.
* - `'transform'` (default): writes `transform: translate3d(...)`.
* Promotes items to their own compositor layer — usually smoother on long
* lists, but creates a stacking context and can interfere with
* `position: fixed` descendants. Item elements must still be anchored with
* `position: absolute`, `top: 0`, and `left: 0`.
* - `'position'`: writes `top` / `left`. Item elements must be
* `position: absolute`.
*/
directDomUpdatesMode?: 'position' | 'transform'
}
function useVirtualizerBase<
TScrollElement extends Element | Window,
TItemElement extends Element,
>({
useFlushSync = true,
directDomUpdates = false,
directDomUpdatesMode = 'transform',
...options
}: ReactVirtualizerOptions<TScrollElement, TItemElement>): ReactVirtualizer<
TScrollElement,
TItemElement
> {
const rerender = React.useReducer((x: number) => x + 1, 0)[1]
// Mutable across renders so the onChange closure captured by setOptions
// always reads the latest values without us having to re-create it.
const directRef = React.useRef({
enabled: directDomUpdates,
mode: directDomUpdatesMode,
container: null as HTMLElement | null,
lastSize: null as number | null,
// Keyed by the element itself so a remounted node (same key, new DOM
// node — e.g. when `enabled` is toggled off then on) is treated as fresh
// and gets its style written.
lastPositions: new WeakMap<HTMLElement, number>(),
prevRange: null as {
startIndex: number
endIndex: number
isScrolling: boolean
} | null,
})
directRef.current.enabled = directDomUpdates
directRef.current.mode = directDomUpdatesMode
// Writes container size + item positions to the DOM. Idempotent — guarded
// by lastSize / lastPositions. Called from onChange (covers scroll-driven
// updates) and from a layout effect (covers post-render commits when refs
// have just registered new items in elementsCache).
const applyDirectStyles = (
instance: Virtualizer<TScrollElement, TItemElement>,
) => {
const state = directRef.current
if (!state.enabled) return
const totalSize = instance.getTotalSize()
if (state.container && totalSize !== state.lastSize) {
state.lastSize = totalSize
const sizeAxis = instance.options.horizontal ? 'width' : 'height'
state.container.style[sizeAxis] = `${totalSize}px`
}
const horizontal = !!instance.options.horizontal
const useTransform = state.mode === 'transform'
const posAxis = horizontal ? 'left' : 'top'
const scrollMargin = instance.options.scrollMargin
const items = instance.getVirtualItems()
for (const item of items) {
const next = item.start - scrollMargin
const el = instance.elementsCache.get(item.key) as HTMLElement | undefined
if (!el) continue
if (state.lastPositions.get(el) === next) continue
state.lastPositions.set(el, next)
if (useTransform) {
el.style.transform = horizontal
? `translate3d(${next}px, 0, 0)`
: `translate3d(0, ${next}px, 0)`
} else {
el.style[posAxis] = `${next}px`
}
}
}
const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> = {
...options,
onChange: (instance, sync) => {
const state = directRef.current
let shouldRerender = true
if (state.enabled) {
applyDirectStyles(instance)
// Only re-render on range / isScrolling changes
const range = instance.range
const prev = state.prevRange
shouldRerender =
!prev ||
prev.isScrolling !== instance.isScrolling ||
prev.startIndex !== range?.startIndex ||
prev.endIndex !== range?.endIndex
if (shouldRerender) {
state.prevRange = range
? {
startIndex: range.startIndex,
endIndex: range.endIndex,
isScrolling: instance.isScrolling,
}
: null
}
}
if (shouldRerender) {
if (useFlushSync && sync) {
flushSync(rerender)
} else {
rerender()
}
}
options.onChange?.(instance, sync)
},
}
const [instance] = React.useState(() => {
const v = new Virtualizer<TScrollElement, TItemElement>(resolvedOptions)
return Object.assign(v, {
containerRef: (node: HTMLElement | null) => {
const state = directRef.current
state.container = node
state.lastSize = null
if (node && state.enabled) {
const total = v.getTotalSize()
state.lastSize = total
const axis = v.options.horizontal ? 'width' : 'height'
node.style[axis] = `${total}px`
}
},
})
})
instance.setOptions(resolvedOptions)
useIsomorphicLayoutEffect(() => {
return instance._didMount()
}, [])
useIsomorphicLayoutEffect(() => {
return instance._willUpdate()
})
// After every render commit, newly mounted item refs have registered in
// elementsCache; write their positions to the DOM so the user doesn't see
// them at (0, 0) until the next onChange.
useIsomorphicLayoutEffect(() => {
applyDirectStyles(instance)
})
return instance
}
export function useVirtualizer<
TScrollElement extends Element,
TItemElement extends Element,
>(
options: PartialKeys<
ReactVirtualizerOptions<TScrollElement, TItemElement>,
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
>,
): ReactVirtualizer<TScrollElement, TItemElement> {
return useVirtualizerBase<TScrollElement, TItemElement>({
observeElementRect: observeElementRect,
observeElementOffset: observeElementOffset,
scrollToFn: elementScroll,
...options,
})
}
export function useWindowVirtualizer<TItemElement extends Element>(
options: PartialKeys<
ReactVirtualizerOptions<Window, TItemElement>,
| 'getScrollElement'
| 'observeElementRect'
| 'observeElementOffset'
| 'scrollToFn'
>,
): ReactVirtualizer<Window, TItemElement> {
return useVirtualizerBase<Window, TItemElement>({
getScrollElement: () => (typeof document !== 'undefined' ? window : null),
observeElementRect: observeWindowRect,
observeElementOffset: observeWindowOffset,
scrollToFn: windowScroll,
initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0),
...options,
})
}