From 3fea3b75c3b364a60b13b1019c5162047d796519 Mon Sep 17 00:00:00 2001 From: Damian Pieczynski Date: Sat, 3 Jan 2026 21:04:51 +0100 Subject: [PATCH 1/4] fix(virtual-core): wait extra frame for ResizeObserver before verifying position --- packages/virtual-core/src/index.ts | 31 +++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index 7f33a2d8..bb608791 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -359,6 +359,7 @@ export class Virtualizer< scrollElement: TScrollElement | null = null targetWindow: (Window & typeof globalThis) | null = null isScrolling = false + private currentScrollToIndex: number | null = null measurementsCache: Array = [] private itemSizeCache = new Map() private laneAssignments = new Map() // index → lane cache @@ -1085,6 +1086,7 @@ export class Virtualizer< } index = Math.max(0, Math.min(index, this.options.count - 1)) + this.currentScrollToIndex = index let attempts = 0 const maxAttempts = 10 @@ -1101,15 +1103,27 @@ export class Virtualizer< this._scrollToOffset(offset, { adjustments: undefined, behavior }) this.targetWindow.requestAnimationFrame(() => { - const currentOffset = this.getScrollOffset() - const afterInfo = this.getOffsetForIndex(index, align) - if (!afterInfo) { - console.warn('Failed to get offset for index:', index) - return + const verify = () => { + // Abort if a new scrollToIndex was called with a different index + if (this.currentScrollToIndex !== index) return + + const currentOffset = this.getScrollOffset() + const afterInfo = this.getOffsetForIndex(index, align) + if (!afterInfo) { + console.warn('Failed to get offset for index:', index) + return + } + + if (!approxEqual(afterInfo[0], currentOffset)) { + scheduleRetry(align) + } } - if (!approxEqual(afterInfo[0], currentOffset)) { - scheduleRetry(align) + // In dynamic mode, wait an extra frame for ResizeObserver to measure newly visible elements + if (this.isDynamicMode()) { + this.targetWindow!.requestAnimationFrame(verify) + } else { + verify() } }) } @@ -1117,6 +1131,9 @@ export class Virtualizer< const scheduleRetry = (align: ScrollAlignment) => { if (!this.targetWindow) return + // Abort if a new scrollToIndex was called with a different index + if (this.currentScrollToIndex !== index) return + attempts++ if (attempts < maxAttempts) { if (process.env.NODE_ENV !== 'production' && this.options.debug) { From 028c647d80ee3a3a89bbd25ec64bba7bcf407802 Mon Sep 17 00:00:00 2001 From: Damian Pieczynski Date: Sat, 3 Jan 2026 21:13:03 +0100 Subject: [PATCH 2/4] chore: add changeset Improve reliability of scrollToIndex in dynamic mode by waiting for ResizeObserver measurements and aborting pending scroll operations. --- .changeset/twenty-maps-fry.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/twenty-maps-fry.md diff --git a/.changeset/twenty-maps-fry.md b/.changeset/twenty-maps-fry.md new file mode 100644 index 00000000..4cca5645 --- /dev/null +++ b/.changeset/twenty-maps-fry.md @@ -0,0 +1,9 @@ +--- +'@tanstack/virtual-core': patch +--- + +fix(virtual-core): improve scrollToIndex reliability in dynamic mode + +- Wait extra frame for ResizeObserver measurements before verifying position +- Abort pending scroll operations when new scrollToIndex is called + From 5db447266cbf25ba7d34f54ff790670c7aa3e609 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:13:35 +0000 Subject: [PATCH 3/4] ci: apply automated fixes --- .changeset/twenty-maps-fry.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/twenty-maps-fry.md b/.changeset/twenty-maps-fry.md index 4cca5645..70e50788 100644 --- a/.changeset/twenty-maps-fry.md +++ b/.changeset/twenty-maps-fry.md @@ -6,4 +6,3 @@ fix(virtual-core): improve scrollToIndex reliability in dynamic mode - Wait extra frame for ResizeObserver measurements before verifying position - Abort pending scroll operations when new scrollToIndex is called - From 231b63a8200e6550ca8e5988b0a51954bb56ad5a Mon Sep 17 00:00:00 2001 From: Damian Pieczynski Date: Sat, 3 Jan 2026 21:39:07 +0100 Subject: [PATCH 4/4] fix(virtual-core): fix scroll position restoration when element changes --- packages/virtual-core/src/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index bb608791..efbf1e54 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -178,7 +178,6 @@ export const observeElementOffset = ( } const handler = createHandler(true) const endHandler = createHandler(false) - endHandler() element.addEventListener('scroll', handler, addEventListenerOptions) const registerScrollendEvent = @@ -226,7 +225,6 @@ export const observeWindowOffset = ( } const handler = createHandler(true) const endHandler = createHandler(false) - endHandler() element.addEventListener('scroll', handler, addEventListenerOptions) const registerScrollendEvent = @@ -519,11 +517,6 @@ export class Virtualizer< this.observer.observe(cached) }) - this._scrollToOffset(this.getScrollOffset(), { - adjustments: undefined, - behavior: undefined, - }) - this.unsubs.push( this.options.observeElementRect(this, (rect) => { this.scrollRect = rect @@ -545,6 +538,11 @@ export class Virtualizer< this.maybeNotify() }), ) + + this._scrollToOffset(this.getScrollOffset(), { + adjustments: undefined, + behavior: undefined, + }) } }