Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/src/pages/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ const {
- Optional
- This function, if passed, is responsible for implementing the scrollTo logic for the parentRef which is used when methods like `scrollToOffset` and `scrollToIndex` are called.
- Eg. You can use this function to implement smooth scrolling by using the supplied offset and the `defaultScrollToFn` as seen in the sandbox's **Smooth Scroll** example.
- `useObserver: Function(parentRef) => ({ width: number; height: number })`
- `initialRect: Object({ width: number; height: number })`
- Optional
- Defines initial rect size of list. Can be used for server-side rendering
- `useObserver: Function(parentRef: React.useRef(DOMElement), initialRect?: { width: number; height: number }) => ({ width: number; height: number })`
- Optional
- This hook, if passed, is responsible for getting `parentRef`'s dimensions
- Eg. You can use this hook to replace [@reach/observe-rect](https://github.com/reach/observe-rect) that `react-virtual` uses by default with [ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
Expand Down
87 changes: 31 additions & 56 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'

import useRect from './useRect'
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'

Expand Down Expand Up @@ -36,6 +35,7 @@ export function useVirtual({
horizontal,
scrollToFn,
useObserver,
initialRect,
onScrollElement,
scrollOffsetFn,
keyExtractor = defaultKeyExtractor,
Expand All @@ -44,15 +44,19 @@ export function useVirtual({
}) {
const sizeKey = horizontal ? 'width' : 'height'
const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'

const latestRef = React.useRef({
scrollOffset: 0,
measurements: [],
})

const [scrollOffset, setScrollOffset] = React.useState(0)
latestRef.current.scrollOffset = scrollOffset

const useMeasureParent = useObserver || useRect

const { [sizeKey]: outerSize } = useMeasureParent(parentRef) || {
[sizeKey]: 0,
}
const { [sizeKey]: outerSize } = useMeasureParent(parentRef, initialRect)

latestRef.current.outerSize = outerSize

const defaultScrollToFn = React.useCallback(
Expand Down Expand Up @@ -105,49 +109,26 @@ export function useVirtual({
latestRef.current.measurements = measurements
latestRef.current.totalSize = totalSize

const [range, setRange] = React.useState({ start: 0, end: 0 })

const element = onScrollElement ? onScrollElement.current : parentRef.current

const scrollOffsetFnRef = React.useRef(scrollOffsetFn)
scrollOffsetFnRef.current = scrollOffsetFn

const rangeTimeoutIdRef = React.useRef(null)

const cancelAsyncRange = React.useCallback(() => {
if (rangeTimeoutIdRef.current !== null) {
clearTimeout(rangeTimeoutIdRef.current)
rangeTimeoutIdRef.current = null
}
}, [])

useIsomorphicLayoutEffect(() => {
rangeTimeoutIdRef.current = setTimeout(() => {
setRange(prevRange => calculateRange(latestRef.current, prevRange))
})
return () => cancelAsyncRange()
}, [measurements, outerSize, cancelAsyncRange])

useIsomorphicLayoutEffect(() => {
if (!element) {
setRange({ start: 0, end: 0 })
latestRef.current.scrollOffset = 0
setScrollOffset(0)

return
}

const onScroll = event => {
const scrollOffset = scrollOffsetFnRef.current
const offset = scrollOffsetFnRef.current
? scrollOffsetFnRef.current(event)
: element[scrollKey]

latestRef.current.scrollOffset = scrollOffset

cancelAsyncRange()
setRange(prevRange => calculateRange(latestRef.current, prevRange))
setScrollOffset(offset)
}

// Determine initially visible range
onScroll()

element.addEventListener('scroll', onScroll, {
Expand All @@ -158,19 +139,25 @@ export function useVirtual({
return () => {
element.removeEventListener('scroll', onScroll)
}
}, [element, scrollKey, cancelAsyncRange])
}, [element, scrollKey])

const { start, end } = calculateRange(latestRef.current)

const indexes = React.useMemo(
() =>
rangeExtractor({
start,
end,
overscan,
size: measurements.length,
}),
[start, end, overscan, measurements.length, rangeExtractor]
)

const measureSizeRef = React.useRef(measureSize)
measureSizeRef.current = measureSize

const virtualItems = React.useMemo(() => {
const indexes = rangeExtractor({
start: range.start,
end: range.end,
overscan,
size: measurements.length,
})

const virtualItems = []

for (let k = 0, len = indexes.length; k < len; k++) {
Expand Down Expand Up @@ -205,21 +192,13 @@ export function useVirtual({
}

return virtualItems
}, [
defaultScrollToFn,
horizontal,
measurements,
overscan,
range.end,
range.start,
rangeExtractor,
])

const mountedRef = React.useRef()
}, [indexes, defaultScrollToFn, horizontal, measurements])

const mountedRef = React.useRef(false)

useIsomorphicLayoutEffect(() => {
if (mountedRef.current) {
if (estimateSize) setMeasuredCache({})
setMeasuredCache({})
}
mountedRef.current = true
}, [estimateSize])
Expand Down Expand Up @@ -326,7 +305,7 @@ const findNearestBinarySearch = (low, high, getCurrentValue, value) => {
}
}

function calculateRange({ measurements, outerSize, scrollOffset }, prevRange) {
function calculateRange({ measurements, outerSize, scrollOffset }) {
const size = measurements.length - 1
const getOffset = index => measurements[index].start

Expand All @@ -337,9 +316,5 @@ function calculateRange({ measurements, outerSize, scrollOffset }, prevRange) {
end++
}

if (prevRange.start !== start || prevRange.end !== end) {
return { start, end }
}

return prevRange
return { start, end }
}
6 changes: 3 additions & 3 deletions src/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('useVirtual list', () => {
expect(screen.queryByText('Row 4')).toBeInTheDocument()
expect(screen.queryByText('Row 5')).not.toBeInTheDocument()

expect(useVirtual).toHaveBeenCalledTimes(3)
expect(useVirtual).toHaveBeenCalledTimes(1)
})
it('should render with overscan', () => {
render(<List {...props} overscan={0} />)
Expand All @@ -87,7 +87,7 @@ describe('useVirtual list', () => {
expect(screen.queryByText('Row 3')).toBeInTheDocument()
expect(screen.queryByText('Row 4')).not.toBeInTheDocument()

expect(useVirtual).toHaveBeenCalledTimes(3)
expect(useVirtual).toHaveBeenCalledTimes(1)
})
it('should render given dynamic size', async () => {
const onRef = virtualRow => el => {
Expand All @@ -103,7 +103,7 @@ describe('useVirtual list', () => {
await waitFor(() => expect(screen.queryByText('Row 8')).toBeInTheDocument())
expect(screen.queryByText('Row 9')).not.toBeInTheDocument()

expect(useVirtual).toHaveBeenCalledTimes(8)
expect(useVirtual).toHaveBeenCalledTimes(4)
})
it('should render given dynamic size after scroll', async () => {
const onRef = virtualRow => el => {
Expand Down
20 changes: 10 additions & 10 deletions src/useRect.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react'

import observeRect from '@reach/observe-rect'

import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'

export default function useRect(nodeRef) {
export default function useRect(
nodeRef,
initialRect = { width: 0, height: 0 }
) {
const [element, setElement] = React.useState(nodeRef.current)
const [rect, dispatch] = React.useReducer(rectReducer, null)
const [rect, dispatch] = React.useReducer(rectReducer, initialRect)
const initialRectSet = React.useRef(false)

useIsomorphicLayoutEffect(() => {
Expand All @@ -18,8 +19,8 @@ export default function useRect(nodeRef) {
useIsomorphicLayoutEffect(() => {
if (element && !initialRectSet.current) {
initialRectSet.current = true
const rect = element.getBoundingClientRect();
dispatch({ rect });
const rect = element.getBoundingClientRect()
dispatch({ rect })
}
}, [element])

Expand All @@ -29,8 +30,8 @@ export default function useRect(nodeRef) {
}

const observer = observeRect(element, rect => {
dispatch({ rect });
});
dispatch({ rect })
})

observer.observe()

Expand All @@ -44,9 +45,8 @@ export default function useRect(nodeRef) {

function rectReducer(state, action) {
const rect = action.rect
if (!state || state.height !== rect.height || state.width !== rect.width) {
if (state.height !== rect.height || state.width !== rect.width) {
return rect
}
return state
}

22 changes: 13 additions & 9 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export interface Range {

declare function defaultRangeExtractor(range: Range): number[]

declare function useVirtual<T>(options: {
interface Rect {
width: number
height: number
}

export interface Options<T> {
size: number
parentRef: React.RefObject<T>
estimateSize?: (index: number) => number
Expand All @@ -39,18 +44,17 @@ declare function useVirtual<T>(options: {
) => void
paddingStart?: number
paddingEnd?: number
useObserver?: (
ref: React.RefObject<T>
) => {
width: number
height: number
[key: string]: any
}
useObserver?: (ref: React.RefObject<T>, initialRect?: Rect) => Rect
initialRect?: Rect
keyExtractor?: (index: number) => Key
onScrollElement?: React.RefObject<HTMLElement>
scrollOffsetFn?: (event?: Event) => number
rangeExtractor?: (range: Range) => number[]
}): {
}

declare function useVirtual<T>(
options: Options<T>
): {
virtualItems: VirtualItem[]
totalSize: number
scrollToOffset: (index: number, options?: ScrollToOffsetOptions) => void
Expand Down