Skip to content

Commit db6df21

Browse files
authored
fix(virtual-core): improve scrollToIndex reliability in dynamic mode
1 parent 0bcf14d commit db6df21

File tree

2 files changed

+37
-14
lines changed

2 files changed

+37
-14
lines changed

.changeset/twenty-maps-fry.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@tanstack/virtual-core': patch
3+
---
4+
5+
fix(virtual-core): improve scrollToIndex reliability in dynamic mode
6+
7+
- Wait extra frame for ResizeObserver measurements before verifying position
8+
- Abort pending scroll operations when new scrollToIndex is called

packages/virtual-core/src/index.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ export const observeElementOffset = <T extends Element>(
178178
}
179179
const handler = createHandler(true)
180180
const endHandler = createHandler(false)
181-
endHandler()
182181

183182
element.addEventListener('scroll', handler, addEventListenerOptions)
184183
const registerScrollendEvent =
@@ -226,7 +225,6 @@ export const observeWindowOffset = (
226225
}
227226
const handler = createHandler(true)
228227
const endHandler = createHandler(false)
229-
endHandler()
230228

231229
element.addEventListener('scroll', handler, addEventListenerOptions)
232230
const registerScrollendEvent =
@@ -359,6 +357,7 @@ export class Virtualizer<
359357
scrollElement: TScrollElement | null = null
360358
targetWindow: (Window & typeof globalThis) | null = null
361359
isScrolling = false
360+
private currentScrollToIndex: number | null = null
362361
measurementsCache: Array<VirtualItem> = []
363362
private itemSizeCache = new Map<Key, number>()
364363
private laneAssignments = new Map<number, number>() // index → lane cache
@@ -518,11 +517,6 @@ export class Virtualizer<
518517
this.observer.observe(cached)
519518
})
520519

521-
this._scrollToOffset(this.getScrollOffset(), {
522-
adjustments: undefined,
523-
behavior: undefined,
524-
})
525-
526520
this.unsubs.push(
527521
this.options.observeElementRect(this, (rect) => {
528522
this.scrollRect = rect
@@ -544,6 +538,11 @@ export class Virtualizer<
544538
this.maybeNotify()
545539
}),
546540
)
541+
542+
this._scrollToOffset(this.getScrollOffset(), {
543+
adjustments: undefined,
544+
behavior: undefined,
545+
})
547546
}
548547
}
549548

@@ -1085,6 +1084,7 @@ export class Virtualizer<
10851084
}
10861085

10871086
index = Math.max(0, Math.min(index, this.options.count - 1))
1087+
this.currentScrollToIndex = index
10881088

10891089
let attempts = 0
10901090
const maxAttempts = 10
@@ -1101,22 +1101,37 @@ export class Virtualizer<
11011101
this._scrollToOffset(offset, { adjustments: undefined, behavior })
11021102

11031103
this.targetWindow.requestAnimationFrame(() => {
1104-
const currentOffset = this.getScrollOffset()
1105-
const afterInfo = this.getOffsetForIndex(index, align)
1106-
if (!afterInfo) {
1107-
console.warn('Failed to get offset for index:', index)
1108-
return
1104+
const verify = () => {
1105+
// Abort if a new scrollToIndex was called with a different index
1106+
if (this.currentScrollToIndex !== index) return
1107+
1108+
const currentOffset = this.getScrollOffset()
1109+
const afterInfo = this.getOffsetForIndex(index, align)
1110+
if (!afterInfo) {
1111+
console.warn('Failed to get offset for index:', index)
1112+
return
1113+
}
1114+
1115+
if (!approxEqual(afterInfo[0], currentOffset)) {
1116+
scheduleRetry(align)
1117+
}
11091118
}
11101119

1111-
if (!approxEqual(afterInfo[0], currentOffset)) {
1112-
scheduleRetry(align)
1120+
// In dynamic mode, wait an extra frame for ResizeObserver to measure newly visible elements
1121+
if (this.isDynamicMode()) {
1122+
this.targetWindow!.requestAnimationFrame(verify)
1123+
} else {
1124+
verify()
11131125
}
11141126
})
11151127
}
11161128

11171129
const scheduleRetry = (align: ScrollAlignment) => {
11181130
if (!this.targetWindow) return
11191131

1132+
// Abort if a new scrollToIndex was called with a different index
1133+
if (this.currentScrollToIndex !== index) return
1134+
11201135
attempts++
11211136
if (attempts < maxAttempts) {
11221137
if (process.env.NODE_ENV !== 'production' && this.options.debug) {

0 commit comments

Comments
 (0)