Skip to content

Commit 380be13

Browse files
authored
fix: measureRef infinite loop, with offsetWidth/offsetHeight configurable by measureSize (#160)
1 parent 954e8d7 commit 380be13

File tree

2 files changed

+63
-47
lines changed

2 files changed

+63
-47
lines changed

src/index.js

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

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

10+
const defaultMeasureSize = (el, horizontal) => {
11+
const key = horizontal ? 'offsetWidth' : 'offsetHeight'
12+
13+
return el[key]
14+
}
15+
916
export function useVirtual({
1017
size = 0,
1118
estimateSize = defaultEstimateSize,
@@ -19,6 +26,7 @@ export function useVirtual({
1926
onScrollElement,
2027
scrollOffsetFn,
2128
keyExtractor = defaultKeyExtractor,
29+
measureSize = defaultMeasureSize,
2230
}) {
2331
const sizeKey = horizontal ? 'width' : 'height'
2432
const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'
@@ -108,6 +116,9 @@ export function useVirtual({
108116
}
109117
}, [element, scrollKey, size, outerSize])
110118

119+
const measureSizeRef = React.useRef(measureSize)
120+
measureSizeRef.current = measureSize
121+
111122
const virtualItems = React.useMemo(() => {
112123
const virtualItems = []
113124
const end = Math.min(range.end, measurements.length - 1)
@@ -118,12 +129,12 @@ export function useVirtual({
118129
const item = {
119130
...measurement,
120131
measureRef: el => {
121-
const { scrollOffset } = latestRef.current
122-
123132
if (el) {
124-
const { [sizeKey]: measuredSize } = el.getBoundingClientRect()
133+
const measuredSize = measureSizeRef.current(el, horizontal)
125134

126135
if (measuredSize !== item.size) {
136+
const { scrollOffset } = latestRef.current
137+
127138
if (item.start < scrollOffset) {
128139
defaultScrollToFn(scrollOffset + (measuredSize - item.size))
129140
}
@@ -145,7 +156,7 @@ export function useVirtual({
145156
range.start,
146157
range.end,
147158
measurements,
148-
sizeKey,
159+
horizontal,
149160
defaultScrollToFn,
150161
keyExtractor,
151162
])

src/tests/index.test.js

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import '@testing-library/jest-dom'
22
import { render, screen, fireEvent } from '@testing-library/react'
33
import * as React from 'react'
44

5-
import { useVirtual } from '../index'
5+
import { useVirtual as useVirtualImpl } from '../index'
66

77
function List({
88
size = 200,
99
overscan,
1010
height = 200,
1111
width = 200,
12-
getBoundingClientRect,
13-
parentRef = React.createRef(),
12+
onRef,
13+
parentRef,
14+
useVirtual,
1415
}) {
1516
const rowVirtualizer = useVirtual({
1617
size,
@@ -39,27 +40,14 @@ function List({
3940
{rowVirtualizer.virtualItems.map(virtualRow => (
4041
<div
4142
key={virtualRow.index}
42-
ref={
43-
getBoundingClientRect
44-
? el => {
45-
if (el) {
46-
el.getBoundingClientRect = getBoundingClientRect(
47-
virtualRow.index
48-
)
49-
}
50-
virtualRow.measureRef(el)
51-
}
52-
: undefined
53-
}
43+
ref={onRef ? onRef(virtualRow) : undefined}
5444
style={{
5545
position: 'absolute',
5646
top: 0,
5747
left: 0,
5848
width: '100%',
5949
transform: `translateY(${virtualRow.start}px)`,
60-
...(getBoundingClientRect
61-
? {}
62-
: { height: `${virtualRow.size}px` }),
50+
height: `${virtualRow.size}px`,
6351
}}
6452
>
6553
Row {virtualRow.index}
@@ -72,50 +60,67 @@ function List({
7260
}
7361

7462
describe('useVirtual list', () => {
75-
it('should render', async () => {
76-
render(<List />)
63+
let useVirtual, parentRef, props
64+
65+
beforeEach(() => {
66+
parentRef = React.createRef()
67+
useVirtual = jest.fn(props => useVirtualImpl(props))
68+
69+
props = { parentRef, useVirtual }
70+
})
71+
72+
it('should render', () => {
73+
render(<List {...props} />)
7774

7875
expect(screen.queryByText('Row 0')).toBeInTheDocument()
76+
expect(screen.queryByText('Row 4')).toBeInTheDocument()
7977
expect(screen.queryByText('Row 5')).not.toBeInTheDocument()
78+
79+
expect(useVirtual).toHaveBeenCalledTimes(3)
8080
})
81-
it('should render with overscan', async () => {
82-
render(<List overscan={0} />)
81+
it('should render with overscan', () => {
82+
render(<List {...props} overscan={0} />)
8383

8484
expect(screen.queryByText('Row 0')).toBeInTheDocument()
85+
expect(screen.queryByText('Row 3')).toBeInTheDocument()
8586
expect(screen.queryByText('Row 4')).not.toBeInTheDocument()
87+
88+
expect(useVirtual).toHaveBeenCalledTimes(3)
8689
})
87-
it('should render given dynamic size', async () => {
88-
const getBoundingClientRect = index =>
89-
jest.fn(() => ({
90-
height: index % 2 === 0 ? 25 : 50,
91-
}))
90+
it('should render given dynamic size', () => {
91+
const onRef = virtualRow => el => {
92+
if (el) {
93+
jest.spyOn(el, 'offsetHeight', 'get').mockImplementation(() => 25)
94+
}
95+
virtualRow.measureRef(el)
96+
}
9297

93-
render(<List getBoundingClientRect={getBoundingClientRect} />)
98+
render(<List {...props} onRef={onRef} />)
9499

95100
expect(screen.queryByText('Row 0')).toBeInTheDocument()
101+
expect(screen.queryByText('Row 5')).toBeInTheDocument()
96102
expect(screen.queryByText('Row 6')).not.toBeInTheDocument()
97-
})
98103

99-
it('should render given dynamic size after scroll', async () => {
100-
const getBoundingClientRect = index =>
101-
jest.fn(() => ({
102-
height: index % 2 === 0 ? 25 : 50,
103-
}))
104-
const parentRef = React.createRef()
104+
expect(useVirtual).toHaveBeenCalledTimes(4)
105+
})
106+
it('should render given dynamic size after scroll', () => {
107+
const onRef = virtualRow => el => {
108+
if (el) {
109+
jest.spyOn(el, 'offsetHeight', 'get').mockImplementation(() => 25)
110+
}
111+
virtualRow.measureRef(el)
112+
}
105113

106-
render(
107-
<List
108-
getBoundingClientRect={getBoundingClientRect}
109-
parentRef={parentRef}
110-
/>
111-
)
114+
render(<List {...props} onRef={onRef} />)
112115

113116
expect(screen.queryByText('Row 0')).toBeInTheDocument()
117+
expect(screen.queryByText('Row 5')).toBeInTheDocument()
114118
expect(screen.queryByText('Row 6')).not.toBeInTheDocument()
115119

116120
fireEvent.scroll(parentRef.current, { target: { scrollTop: 375 } })
117121

118-
expect(screen.queryByText('Row 8')).toBeInTheDocument()
119-
expect(screen.queryByText('Row 14')).not.toBeInTheDocument()
122+
expect(screen.queryByText('Row 9')).toBeInTheDocument()
123+
expect(screen.queryByText('Row 15')).toBeInTheDocument()
124+
expect(screen.queryByText('Row 16')).not.toBeInTheDocument()
120125
})
121126
})

0 commit comments

Comments
 (0)