diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 6f4e834be..e8228cd7f 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -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)
diff --git a/src/index.js b/src/index.js
index d53a71290..7576a3f01 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,4 @@
import React from 'react'
-
import useRect from './useRect'
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
@@ -36,6 +35,7 @@ export function useVirtual({
horizontal,
scrollToFn,
useObserver,
+ initialRect,
onScrollElement,
scrollOffsetFn,
keyExtractor = defaultKeyExtractor,
@@ -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(
@@ -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, {
@@ -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++) {
@@ -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])
@@ -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
@@ -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 }
}
diff --git a/src/tests/index.test.js b/src/tests/index.test.js
index c19586e8a..4d93c5754 100644
--- a/src/tests/index.test.js
+++ b/src/tests/index.test.js
@@ -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(
)
@@ -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 => {
@@ -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 => {
diff --git a/src/useRect.js b/src/useRect.js
index 1b6796083..113b1fa96 100644
--- a/src/useRect.js
+++ b/src/useRect.js
@@ -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(() => {
@@ -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])
@@ -29,8 +30,8 @@ export default function useRect(nodeRef) {
}
const observer = observeRect(element, rect => {
- dispatch({ rect });
- });
+ dispatch({ rect })
+ })
observer.observe()
@@ -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
}
-
diff --git a/types/index.d.ts b/types/index.d.ts
index 41b176d4a..14f41bc6f 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -27,7 +27,12 @@ export interface Range {
declare function defaultRangeExtractor(range: Range): number[]
-declare function useVirtual(options: {
+interface Rect {
+ width: number
+ height: number
+}
+
+export interface Options {
size: number
parentRef: React.RefObject
estimateSize?: (index: number) => number
@@ -39,18 +44,17 @@ declare function useVirtual(options: {
) => void
paddingStart?: number
paddingEnd?: number
- useObserver?: (
- ref: React.RefObject
- ) => {
- width: number
- height: number
- [key: string]: any
- }
+ useObserver?: (ref: React.RefObject, initialRect?: Rect) => Rect
+ initialRect?: Rect
keyExtractor?: (index: number) => Key
onScrollElement?: React.RefObject
scrollOffsetFn?: (event?: Event) => number
rangeExtractor?: (range: Range) => number[]
-}): {
+}
+
+declare function useVirtual(
+ options: Options
+): {
virtualItems: VirtualItem[]
totalSize: number
scrollToOffset: (index: number, options?: ScrollToOffsetOptions) => void