11import React from 'react'
22
3- import useScroll from './useScroll'
43import useRect from './useRect'
54import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
65
@@ -18,23 +17,15 @@ export function useVirtual({
1817} ) {
1918 const sizeKey = horizontal ? 'width' : 'height'
2019 const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'
20+ const latestRef = React . useRef ( { } )
2121
2222 const { [ sizeKey ] : outerSize } = useRect ( parentRef ) || {
2323 [ sizeKey ] : 0 ,
2424 }
2525
26- const [ scrollOffset , _setScrollOffset ] = React . useState ( 0 )
27-
28- const scrollOffsetPlusOuterSize = scrollOffset + outerSize
29-
30- useScroll ( parentRef , ( { [ scrollKey ] : newScrollOffset } ) => {
31- _setScrollOffset ( newScrollOffset )
32- } )
33-
3426 const defaultScrollToFn = React . useCallback (
3527 offset => {
3628 if ( parentRef . current ) {
37- _setScrollOffset ( offset )
3829 parentRef . current [ scrollKey ] = offset
3930 }
4031 } ,
@@ -52,69 +43,56 @@ export function useVirtual({
5243
5344 const [ measuredCache , setMeasuredCache ] = React . useState ( { } )
5445
55- const { measurements, reversedMeasurements } = React . useMemo ( ( ) => {
46+ const measurements = React . useMemo ( ( ) => {
5647 const measurements = [ ]
57- const reversedMeasurements = [ ]
58-
59- for ( let i = 0 , j = size - 1 ; i < size ; i ++ , j -- ) {
48+ for ( let i = 0 ; i < size ; i ++ ) {
6049 const measuredSize = measuredCache [ i ]
6150 const start = measurements [ i - 1 ] ? measurements [ i - 1 ] . end : paddingStart
6251 const size =
6352 typeof measuredSize === 'number' ? measuredSize : estimateSize ( i )
6453 const end = start + size
65- const bounds = { index : i , start, size, end }
66- measurements [ i ] = {
67- ...bounds ,
68- }
69- reversedMeasurements [ j ] = {
70- ...bounds ,
71- }
54+ measurements [ i ] = { index : i , start, size, end }
7255 }
73- return { measurements, reversedMeasurements }
56+ return measurements
7457 } , [ estimateSize , measuredCache , paddingStart , size ] )
7558
7659 const totalSize = ( measurements [ size - 1 ] ?. end || 0 ) + paddingEnd
7760
78- let start = React . useMemo (
79- ( ) =>
80- reversedMeasurements . reduce (
81- ( last , rowStat ) => ( rowStat . end >= scrollOffset ? rowStat : last ) ,
82- reversedMeasurements [ 0 ]
83- ) ,
84- [ reversedMeasurements , scrollOffset ]
85- )
61+ Object . assign ( latestRef . current , {
62+ overscan,
63+ measurements,
64+ outerSize,
65+ totalSize,
66+ } )
8667
87- let end = React . useMemo (
88- ( ) =>
89- measurements . reduce (
90- ( last , rowStat ) =>
91- rowStat . start <= scrollOffsetPlusOuterSize ? rowStat : last ,
92- measurements [ 0 ]
93- ) ,
94- [ measurements , scrollOffsetPlusOuterSize ]
95- )
68+ const [ range , setRange ] = React . useState ( { start : 0 , end : 0 } )
9669
97- let startIndex = start ? start . index : 0
98- let endIndex = end ? end . index : 0
70+ useIsomorphicLayoutEffect ( ( ) => {
71+ const element = parentRef . current
9972
100- // Always add at least one overscan item, so focus will work
101- startIndex = Math . max ( startIndex - overscan , 0 )
102- endIndex = Math . min ( endIndex + overscan , size - 1 )
73+ const onScroll = ( ) => {
74+ const scrollOffset = element [ scrollKey ]
75+ latestRef . current . scrollOffset = scrollOffset
76+ setRange ( prevRange => calculateRange ( latestRef . current , prevRange ) )
77+ }
10378
104- const latestRef = React . useRef ( { } )
79+ // Determine initially visible range
80+ onScroll ( )
10581
106- latestRef . current = {
107- measurements,
108- outerSize,
109- scrollOffset,
110- scrollOffsetPlusOuterSize,
111- totalSize,
112- }
82+ element . addEventListener ( 'scroll' , onScroll , {
83+ capture : false ,
84+ passive : true ,
85+ } )
86+
87+ return ( ) => {
88+ element . removeEventListener ( 'scroll' , onScroll )
89+ }
90+ } , [ parentRef . current , scrollKey , size /* required */ ] )
11391
11492 const virtualItems = React . useMemo ( ( ) => {
11593 const virtualItems = [ ]
11694
117- for ( let i = startIndex ; i <= endIndex ; i ++ ) {
95+ for ( let i = range . start ; i <= range . end ; i ++ ) {
11896 const measurement = measurements [ i ]
11997
12098 const item = {
@@ -143,7 +121,7 @@ export function useVirtual({
143121 }
144122
145123 return virtualItems
146- } , [ startIndex , endIndex , measurements , sizeKey , defaultScrollToFn ] )
124+ } , [ range . start , range . end , measurements , sizeKey , defaultScrollToFn ] )
147125
148126 const mountedRef = React . useRef ( )
149127
@@ -156,16 +134,12 @@ export function useVirtual({
156134
157135 const scrollToOffset = React . useCallback (
158136 ( toOffset , { align = 'start' } = { } ) => {
159- const {
160- outerSize,
161- scrollOffset,
162- scrollOffsetPlusOuterSize,
163- } = latestRef . current
137+ const { scrollOffset, outerSize } = latestRef . current
164138
165139 if ( align === 'auto' ) {
166140 if ( toOffset <= scrollOffset ) {
167141 align = 'start'
168- } else if ( scrollOffset >= scrollOffsetPlusOuterSize ) {
142+ } else if ( scrollOffset >= scrollOffset + outerSize ) {
169143 align = 'end'
170144 } else {
171145 align = 'start'
@@ -185,11 +159,7 @@ export function useVirtual({
185159
186160 const tryScrollToIndex = React . useCallback (
187161 ( index , { align = 'auto' , ...rest } = { } ) => {
188- const {
189- measurements,
190- scrollOffset,
191- scrollOffsetPlusOuterSize,
192- } = latestRef . current
162+ const { measurements, scrollOffset, outerSize } = latestRef . current
193163
194164 const measurement = measurements [ Math . max ( 0 , Math . min ( index , size - 1 ) ) ]
195165
@@ -198,7 +168,7 @@ export function useVirtual({
198168 }
199169
200170 if ( align === 'auto' ) {
201- if ( measurement . end >= scrollOffsetPlusOuterSize ) {
171+ if ( measurement . end >= scrollOffset + outerSize ) {
202172 align = 'end'
203173 } else if ( measurement . start <= scrollOffset ) {
204174 align = 'start'
@@ -241,3 +211,30 @@ export function useVirtual({
241211 scrollToIndex,
242212 }
243213}
214+
215+ function calculateRange ( {
216+ overscan,
217+ measurements,
218+ outerSize,
219+ scrollOffset,
220+ } , prevRange ) {
221+ const total = measurements . length
222+ let start = total - 1
223+ while ( start > 0 && measurements [ start ] . end >= scrollOffset ) {
224+ start -= 1
225+ }
226+ let end = 0
227+ while ( end < total - 1 && measurements [ end ] . start <= scrollOffset + outerSize ) {
228+ end += 1
229+ }
230+
231+ // Always add at least one overscan item, so focus will work
232+ start = Math . max ( start - overscan , 0 )
233+ end = Math . min ( end + overscan , total - 1 )
234+
235+ if ( ! prevRange || prevRange . start !== start || prevRange . end !== end ) {
236+ return { start, end }
237+ }
238+
239+ return prevRange
240+ }
0 commit comments