Skip to content

Commit 4256df1

Browse files
authored
feat: support caching dynamic measurements by unique key (#107)
* feat: support caching measurements by unique key * Run Prettier * Update docs
1 parent ea6b541 commit 4256df1

File tree

3 files changed

+82
-66
lines changed

3 files changed

+82
-66
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ const {
284284
- `scrollOffsetFn: Function() => number`
285285
- Optional
286286
- This function, if passed, is called on scroll to get the scroll offest rather than using `parentRef`'s `width` or `height`
287+
- `keyExtractor: Function(index) => String | Integer`
288+
- Optional
289+
- This function receives the index of each item and should return the item's unique ID.
290+
- This function should be passed whenever dynamic measurement rendering is enabled and the size or order of items in the list changes.
287291

288292
### Returns
289293

src/index.js

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import useRect from './useRect'
44
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
55

66
const defaultEstimateSize = () => 50
7+
const defaultKeyExtractor = index => index
78

89
export function useVirtual({
910
size = 0,
@@ -17,6 +18,7 @@ export function useVirtual({
1718
useObserver,
1819
onScrollElement,
1920
scrollOffsetFn,
21+
keyExtractor = defaultKeyExtractor,
2022
}) {
2123
const sizeKey = horizontal ? 'width' : 'height'
2224
const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'
@@ -47,23 +49,20 @@ export function useVirtual({
4749

4850
const [measuredCache, setMeasuredCache] = React.useState({})
4951

50-
const measure = React.useCallback(
51-
() => setMeasuredCache({}),
52-
[]
53-
)
52+
const measure = React.useCallback(() => setMeasuredCache({}), [])
5453

5554
const measurements = React.useMemo(() => {
5655
const measurements = []
5756
for (let i = 0; i < size; i++) {
58-
const measuredSize = measuredCache[i]
57+
const measuredSize = measuredCache[keyExtractor(i)]
5958
const start = measurements[i - 1] ? measurements[i - 1].end : paddingStart
6059
const size =
6160
typeof measuredSize === 'number' ? measuredSize : estimateSize(i)
6261
const end = start + size
6362
measurements[i] = { index: i, start, size, end }
6463
}
6564
return measurements
66-
}, [estimateSize, measuredCache, paddingStart, size])
65+
}, [estimateSize, measuredCache, paddingStart, size, keyExtractor])
6766

6867
const totalSize = (measurements[size - 1]?.end || 0) + paddingEnd
6968

@@ -78,10 +77,14 @@ export function useVirtual({
7877

7978
const element = onScrollElement ? onScrollElement.current : parentRef.current
8079
useIsomorphicLayoutEffect(() => {
81-
if (!element) { return }
80+
if (!element) {
81+
return
82+
}
8283

8384
const onScroll = () => {
84-
const scrollOffset = scrollOffsetFn ? scrollOffsetFn() : element[scrollKey]
85+
const scrollOffset = scrollOffsetFn
86+
? scrollOffsetFn()
87+
: element[scrollKey]
8588
latestRef.current.scrollOffset = scrollOffset
8689
setRange(prevRange => calculateRange(latestRef.current, prevRange))
8790
}
@@ -121,7 +124,7 @@ export function useVirtual({
121124

122125
setMeasuredCache(old => ({
123126
...old,
124-
[i]: measuredSize,
127+
[keyExtractor(i)]: measuredSize,
125128
}))
126129
}
127130
}
@@ -132,16 +135,23 @@ export function useVirtual({
132135
}
133136

134137
return virtualItems
135-
}, [range.start, range.end, measurements, sizeKey, defaultScrollToFn])
138+
}, [
139+
range.start,
140+
range.end,
141+
measurements,
142+
sizeKey,
143+
defaultScrollToFn,
144+
keyExtractor,
145+
])
136146

137147
const mountedRef = React.useRef()
138148

139149
useIsomorphicLayoutEffect(() => {
140150
if (mountedRef.current) {
141-
if (estimateSize || size) setMeasuredCache({})
151+
if (estimateSize) setMeasuredCache({})
142152
}
143153
mountedRef.current = true
144-
}, [estimateSize, size])
154+
}, [estimateSize])
145155

146156
const scrollToOffset = React.useCallback(
147157
(toOffset, { align = 'start' } = {}) => {
@@ -192,8 +202,8 @@ export function useVirtual({
192202
align === 'center'
193203
? measurement.start + measurement.size / 2
194204
: align === 'end'
195-
? measurement.end
196-
: measurement.start
205+
? measurement.end
206+
: measurement.start
197207

198208
scrollToOffset(toOffset, { align, ...rest })
199209
},
@@ -224,19 +234,20 @@ export function useVirtual({
224234
}
225235
}
226236

227-
function calculateRange({
228-
overscan,
229-
measurements,
230-
outerSize,
231-
scrollOffset,
232-
}, prevRange) {
237+
function calculateRange(
238+
{ overscan, measurements, outerSize, scrollOffset },
239+
prevRange
240+
) {
233241
const total = measurements.length
234242
let start = total - 1
235243
while (start > 0 && measurements[start].end >= scrollOffset) {
236244
start -= 1
237245
}
238246
let end = 0
239-
while (end < total - 1 && measurements[end].start <= scrollOffset + outerSize) {
247+
while (
248+
end < total - 1 &&
249+
measurements[end].start <= scrollOffset + outerSize
250+
) {
240251
end += 1
241252
}
242253

types/index.d.ts

Lines changed: 46 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,46 @@
1-
type ScrollAlignment = 'start' | 'center' | 'end' | 'auto'
2-
3-
interface ScrollToOptions {
4-
align: ScrollAlignment
5-
}
6-
7-
interface ScrollToOffsetOptions extends ScrollToOptions {}
8-
interface ScrollToIndexOptions extends ScrollToOptions {}
9-
10-
export type VirtualItem = {
11-
index: number
12-
start: number
13-
end: number
14-
size: number
15-
measureRef: React.RefObject<any>
16-
}
17-
18-
declare function useVirtual<T>(options: {
19-
size: number
20-
parentRef: React.RefObject<T>
21-
estimateSize?: (index: number) => number
22-
overscan?: number
23-
horizontal?: boolean
24-
scrollToFn?: (
25-
offset: number,
26-
defaultScrollToFn?: (offset: number) => void
27-
) => void
28-
paddingStart?: number
29-
paddingEnd?: number
30-
useObserver?: (
31-
ref: React.RefObject<T>
32-
) => {
33-
width: number
34-
height: number
35-
[key: string]: any
36-
}
37-
}): {
38-
virtualItems: VirtualItem[]
39-
totalSize: number
40-
scrollToOffset: (index: number, options?: ScrollToOffsetOptions) => void
41-
scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void
42-
measure: () => void
43-
}
44-
45-
export { useVirtual }
1+
type ScrollAlignment = 'start' | 'center' | 'end' | 'auto'
2+
3+
interface ScrollToOptions {
4+
align: ScrollAlignment
5+
}
6+
7+
interface ScrollToOffsetOptions extends ScrollToOptions {}
8+
interface ScrollToIndexOptions extends ScrollToOptions {}
9+
10+
export type VirtualItem = {
11+
index: number
12+
start: number
13+
end: number
14+
size: number
15+
measureRef: React.RefObject<any>
16+
}
17+
18+
declare function useVirtual<T>(options: {
19+
size: number
20+
parentRef: React.RefObject<T>
21+
estimateSize?: (index: number) => number
22+
overscan?: number
23+
horizontal?: boolean
24+
scrollToFn?: (
25+
offset: number,
26+
defaultScrollToFn?: (offset: number) => void
27+
) => void
28+
paddingStart?: number
29+
paddingEnd?: number
30+
useObserver?: (
31+
ref: React.RefObject<T>
32+
) => {
33+
width: number
34+
height: number
35+
[key: string]: any
36+
}
37+
keyExtractor?: (index: number) => number | string
38+
}): {
39+
virtualItems: VirtualItem[]
40+
totalSize: number
41+
scrollToOffset: (index: number, options?: ScrollToOffsetOptions) => void
42+
scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void
43+
measure: () => void
44+
}
45+
46+
export { useVirtual }

0 commit comments

Comments
 (0)