Skip to content

Commit

Permalink
fix: dynamic scrolling in reverse no longer crashes
Browse files Browse the repository at this point in the history
fix: add defaultScrollToFn to scrollFn
perf: faster bounds calculation and virtaulItem generation
  • Loading branch information
tannerlinsley committed May 11, 2020
1 parent 3c280cf commit 2fb37d1
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 52 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ const {
estimateSize,
overscan,
horizontal,
scrollToFn
scrollToFn,
})
```

Expand All @@ -299,10 +299,10 @@ const {
- `horizontal: Boolean`
- Defaults to `false`
- When `true`, this virtualizer will use `width` and `scrollLeft` instead of `height` and `scrollTop` to determine size and offset of virtualized items.
- `scrollToFn: Function(offset) => void 0`
- `scrollToFn: Function(offset, defaultScrollToFn) => void 0`
- Optional
- This function, if passed, is responsible for implementing the scrollTo log for the parentRef.
- Eg. You can use this function implement smooth scrolling by using the supplied offset and animating the parentRef's scroll offset appropriately as seen in the sandbox's **Smooth Scroll** example.
- This function, if passed, is responsible for implementing the scrollTo log for the parentRef which is used when methods like `scrolllToOffset` and `scrollToIndex` are called.
- Eg. You can use this function implement smooth scrolling by using the supplied offset and the `defaultScrollToFn` as seen in the sandbox's **Smooth Scroll** example.

### Returns

Expand Down
114 changes: 68 additions & 46 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,6 @@ export function useVirtual({
const sizeKey = horizontal ? 'width' : 'height'
const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'

const defaultScrollToFn = React.useCallback(
offset => {
parentRef.current[scrollKey] = offset
},
[parentRef, scrollKey]
)

scrollToFn = scrollToFn || defaultScrollToFn

const { [sizeKey]: outerSize } = useRect(parentRef) || {
[sizeKey]: 0,
}
Expand All @@ -36,25 +27,35 @@ export function useVirtual({
_setScrollOffset(newScrollOffset)
})

const scrollOffsetPlusOuterSize = scrollOffset + outerSize
const defaultScrollToFn = React.useCallback(
offset => {
if (parentRef.current) {
_setScrollOffset(offset)
parentRef.current[scrollKey] = offset
}
},
[parentRef, scrollKey]
)

const [measuredCache, setMeasuredCache] = React.useState({})
const resolvedScrollToFn = scrollToFn || defaultScrollToFn

const mountedRef = React.useRef()
scrollToFn = React.useCallback(
offset => {
resolvedScrollToFn(offset, defaultScrollToFn)
},
[defaultScrollToFn, resolvedScrollToFn]
)

useIsomorphicLayoutEffect(() => {
if (mountedRef.current) {
if (estimateSize || size) setMeasuredCache({})
}
mountedRef.current = true
}, [estimateSize, size])
const scrollOffsetPlusOuterSize = scrollOffset + outerSize

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

const { measurements, reversedMeasurements } = React.useMemo(() => {
const measurements = []
const reversedMeasurements = []

for (let i = 0, j = size - 1; i < size; i++, j--) {
const start = measurements[i - 1]?.end || 0
const start = measurements[i - 1] ? measurements[i - 1].end : 0
const size = measuredCache[i] || estimateSize(i)
const end = start + size
const bounds = { index: i, start, size, end }
Expand All @@ -71,26 +72,44 @@ export function useVirtual({
const totalSize = measurements[size - 1]?.end || 0

let start = React.useMemo(
() => measurements.find(rowStat => rowStat.end >= scrollOffset),
[measurements, scrollOffset]
() =>
reversedMeasurements.reduce(
(last, rowStat) => (rowStat.end >= scrollOffset ? rowStat : last),
reversedMeasurements[0]
),
[reversedMeasurements, scrollOffset]
)

let end = React.useMemo(
() =>
reversedMeasurements.find(
rowStat => rowStat.start <= scrollOffsetPlusOuterSize
measurements.reduce(
(last, rowStat) =>
rowStat.start <= scrollOffsetPlusOuterSize ? rowStat : last,
measurements[0]
),
[reversedMeasurements, scrollOffsetPlusOuterSize]
[measurements, scrollOffsetPlusOuterSize]
)

let startIndex = start ? start.index : 0
let endIndex = end ? end.index : 0

// Always add at least one overscan item, so focus will work
startIndex = Math.max(startIndex - 1 - overscan, 0)
endIndex = Math.min(endIndex + 1 + overscan, size - 1)
startIndex = Math.max(startIndex - overscan, 0)
endIndex = Math.min(endIndex + overscan, size - 1)

const latestRef = React.useRef({})

latestRef.current = {
measurements,
outerSize,
scrollOffset,
scrollOffsetPlusOuterSize,
totalSize,
}

const virtualItems = React.useMemo(() => {
const { scrollOffset } = latestRef.current

const virtualItems = []

for (let i = startIndex; i <= endIndex; i++) {
Expand All @@ -99,15 +118,18 @@ export function useVirtual({
const item = {
...measurement,
measureRef: el => {
if (!el) return

const { [sizeKey]: measuredSize } = el.getBoundingClientRect()

if (measuredSize !== item.size) {
setMeasuredCache(old => ({
...old,
[i]: measuredSize,
}))
if (el) {
const { [sizeKey]: measuredSize } = el.getBoundingClientRect()

if (measuredSize !== item.size) {
if (item.start < scrollOffset) {
defaultScrollToFn(scrollOffset + (measuredSize - item.size))
}
setMeasuredCache(old => ({
...old,
[i]: measuredSize,
}))
}
}
},
}
Expand All @@ -116,16 +138,16 @@ export function useVirtual({
}

return virtualItems
}, [startIndex, endIndex, measurements, sizeKey])
}, [startIndex, endIndex, measurements, sizeKey, defaultScrollToFn])

const latestRef = React.useRef()
latestRef.current = {
measurements,
outerSize,
scrollOffset,
scrollOffsetPlusOuterSize,
totalSize,
}
const mountedRef = React.useRef()

useIsomorphicLayoutEffect(() => {
if (mountedRef.current) {
if (estimateSize || size) setMeasuredCache({})
}
mountedRef.current = true
}, [estimateSize, size])

const scrollToOffset = React.useCallback(
(offset, { align = 'start' } = {}) => {
Expand Down Expand Up @@ -160,7 +182,7 @@ export function useVirtual({
)

const scrollToIndex = React.useCallback(
(index, { align = 'auto' } = {}) => {
(index, { align = 'auto', ...rest } = {}) => {
const {
measurements,
scrollOffset,
Expand Down Expand Up @@ -189,7 +211,7 @@ export function useVirtual({
: align === 'end'
? measurement.end
: measurement.start
scrollToOffset(offset, { align })
scrollToOffset(offset, { align, ...rest })
},
[scrollToOffset]
)
Expand Down
7 changes: 5 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ export type VirtualItem = {
declare function useVirtual<T>(options: {
size: number
parentRef: React.RefObject<T>
estimateSize: (index?: number) => number
estimateSize?: (index?: number) => number
overscan?: number
horizontal?: boolean
scrollToFn?: (offset: number) => void
scrollToFn?: (
offset: number,
defaultScrollToFn?: (offset: number) => void
) => void
}): {
virtualItems: VirtualItem[]
totalSize: number
Expand Down

0 comments on commit 2fb37d1

Please sign in to comment.