Skip to content

fix(virtual-core): eagerly adjust scrollOffset on prepend to prevent jump#1176

Merged
tannerlinsley merged 1 commit into
TanStack:mainfrom
piecyk:damian/fix/eager-scroll-anchor-on-prepend
Jun 1, 2026
Merged

fix(virtual-core): eagerly adjust scrollOffset on prepend to prevent jump#1176
tannerlinsley merged 1 commit into
TanStack:mainfrom
piecyk:damian/fix/eager-scroll-anchor-on-prepend

Conversation

@piecyk
Copy link
Copy Markdown
Collaborator

@piecyk piecyk commented May 26, 2026

When items are prepended with anchorTo: 'end' and dynamic sizes, the virtualizer would compute the wrong visible range for one frame (using stale estimate-based positions) and then correct in the next frame via _willUpdate, producing a visible "jump".

Fix by eagerly adjusting scrollOffset in setOptions during the render pass so calculateRange/getVirtualItems return the correct items immediately. _willUpdate then only syncs the browser's actual scroll position to match.

🎯 Changes

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Prevented a visible one-frame jump when items are prepended/appended by eagerly reconciling scroll offset so visible items render consistently.
    • Reduced deferred platform-specific adjustments by synchronizing the browser scroll position immediately when possible, improving render stability (iOS momentum handling deferred adjustments until safe).
  • Tests
    • Updated virtualization tests to reflect the new scroll reconciliation behavior and expected offsets.

@piecyk piecyk requested a review from tannerlinsley May 26, 2026 04:08
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Eagerly resolve append/prepend scroll anchors inside setOptions (precompute anchorDelta and update scrollOffset), record pendingScrollAnchor, and have _willUpdate consume the precomputed delta to sync DOM (with iOS deferral). A changeset and a test expectation were added/updated.

Changes

Scroll Anchor Lifecycle Optimization

Layer / File(s) Summary
Changeset entry for eager scroll anchor
.changeset/eager-scroll-anchor.md
Adds a patch changeset describing the eager scrollOffset adjustment fix for prepend scenarios using anchorTo: 'end' and dynamic sizes.
PendingScrollAnchor tuple update
packages/virtual-core/src/index.ts
PendingScrollAnchor tuple gains an anchorDelta: number element so the delta is computed in setOptions and consumed later in _willUpdate.
Eager anchor resolution in setOptions
packages/virtual-core/src/index.ts
setOptions resolves the anchored item via getMeasurements(), updates this.scrollOffset to anchorItem.start + anchorOffset when resolvable, computes anchorDelta, and stores key/offset/anchorDelta into pendingScrollAnchor.
Pending anchor sync in _willUpdate (and tests)
packages/virtual-core/src/index.ts, packages/virtual-core/tests/index.test.ts
_willUpdate now uses the precomputed anchorDelta: for key !== null and falsy followOnAppend, it either defers adjustment on iOS by adding to _iosDeferredAdjustment or immediately calls _scrollToOffset(getScrollOffset(), { adjustments: undefined, behavior: undefined }); test updated to expect offset: 200 and options.adjustments: undefined.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • TanStack/virtual#1173: Related end-anchoring scroll reconciliation changes affecting setOptions/_willUpdate and tests.

Poem

A rabbit nudges offsets in the night,
Finds anchors true and sets them right,
Eager hops fix a jittered view,
_willUpdate whispers to the DOM too,
Hooray — the list stays steady, woo! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: eagerly adjusting scrollOffset when items are prepended with anchorTo: 'end' to prevent a visible jump.
Description check ✅ Passed The description explains the problem and solution, includes a comprehensive changes section, and has both required checklist sections completed with appropriate selections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 26, 2026

View your CI Pipeline Execution ↗ for commit fc82a0a

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 28s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 13s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-01 17:08:52 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 26, 2026

More templates

@tanstack/angular-virtual

npm i https://pkg.pr.new/@tanstack/angular-virtual@1176

@tanstack/lit-virtual

npm i https://pkg.pr.new/@tanstack/lit-virtual@1176

@tanstack/react-virtual

npm i https://pkg.pr.new/@tanstack/react-virtual@1176

@tanstack/solid-virtual

npm i https://pkg.pr.new/@tanstack/solid-virtual@1176

@tanstack/svelte-virtual

npm i https://pkg.pr.new/@tanstack/svelte-virtual@1176

@tanstack/virtual-core

npm i https://pkg.pr.new/@tanstack/virtual-core@1176

@tanstack/vue-virtual

npm i https://pkg.pr.new/@tanstack/vue-virtual@1176

commit: 8a58d32

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 823-834: Tests expect numeric options.adjustments but
_scrollToOffset now forwards undefined for the _willUpdate anchor sync path;
update the two assertions in packages/virtual-core/tests/index.test.ts that
reference anchorTo:end to expect undefined for options.adjustments instead of
100 and 70 (specifically the tests "anchorTo:end keeps visible content stable
when older items are prepended" and "anchorTo:end keeps a pinned streaming
message pinned as it grows"); leave the iOS deferred-flush test that asserts 50
unchanged because it exercises the iOS accumulator path, and ensure references
to _scrollToOffset and scrollToFn are used when reviewing the behavior change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f21eedb-e3bd-4e6c-a809-62dc0e290649

📥 Commits

Reviewing files that changed from the base of the PR and between 693d915 and f2ec547.

📒 Files selected for processing (1)
  • packages/virtual-core/src/index.ts

Comment thread packages/virtual-core/src/index.ts Outdated
@piecyk piecyk force-pushed the damian/fix/eager-scroll-anchor-on-prepend branch from f2ec547 to 5b972c5 Compare May 26, 2026 05:42
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/virtual-core/tests/index.test.ts (1)

2267-2280: ⚡ Quick win

Assert the eager correction before _willUpdate().

This only proves the later DOM sync. The regression here was the one-frame wrong range during render, so it would be better to assert scrollOffset or getVirtualItems() immediately after setOptions() and before _willUpdate() runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/virtual-core/tests/index.test.ts` around lines 2267 - 2280, The test
currently only checks the post-DOM-sync scrollTo call; add an assertion that
verifies the eager correction happened synchronously before the internal update
by checking the virtualizer's scrollOffset or getVirtualItems() immediately
after calling setMessages (and before _willUpdate() runs). Specifically, after
calling setMessages([{ id: 'm--2' }, { id: 'm--1' }, ...messages]) assert
virtualizer.scrollOffset (or virtualizer.getVirtualItems()) has the expected
value/range to prove the one-frame correction occurred, and keep the existing
scrollToFn assertions for the later DOM sync; locate this in the same test
around the setMessages call and place the new assertion before any code that
triggers or waits for _willUpdate().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 830-838: The direct call to _scrollToOffset bypasses the iOS
deferral path and can be cancelled by a prepend-triggered anchor sync; instead,
gate this DOM sync through the same iOS deferral used elsewhere or use the
existing applyScrollAdjustment path. Replace the direct
this._scrollToOffset(this.getScrollOffset(), ...) with the code path that honors
_iosDeferredAdjustment (i.e., call applyScrollAdjustment / the helper that
defers/queues adjustments when _iosDeferredAdjustment is set) so the scroll
write is deferred during iOS touch/momentum; ensure you still pass the same
offset from getScrollOffset() and preserve the adjustments/behavior options when
delegating.

---

Nitpick comments:
In `@packages/virtual-core/tests/index.test.ts`:
- Around line 2267-2280: The test currently only checks the post-DOM-sync
scrollTo call; add an assertion that verifies the eager correction happened
synchronously before the internal update by checking the virtualizer's
scrollOffset or getVirtualItems() immediately after calling setMessages (and
before _willUpdate() runs). Specifically, after calling setMessages([{ id:
'm--2' }, { id: 'm--1' }, ...messages]) assert virtualizer.scrollOffset (or
virtualizer.getVirtualItems()) has the expected value/range to prove the
one-frame correction occurred, and keep the existing scrollToFn assertions for
the later DOM sync; locate this in the same test around the setMessages call and
place the new assertion before any code that triggers or waits for
_willUpdate().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7da3ba74-d701-4a3a-962b-68f55dc9942b

📥 Commits

Reviewing files that changed from the base of the PR and between f2ec547 and 5b972c5.

📒 Files selected for processing (2)
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

Comment thread packages/virtual-core/src/index.ts Outdated
@piecyk piecyk force-pushed the damian/fix/eager-scroll-anchor-on-prepend branch from 5b972c5 to 2b35365 Compare May 26, 2026 05:51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/virtual-core/src/index.ts (1)

830-838: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve the iOS deferred-scroll path for this DOM sync.

This direct _scrollToOffset write bypasses applyScrollAdjustment's isIOSWebKit() gate. If a prepend lands during touch or momentum scrolling, this path still writes scrollTop immediately and cancels the native scroll on iOS, even though the logical scrollOffset was already fixed eagerly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/virtual-core/src/index.ts` around lines 830 - 838, The direct call
to this._scrollToOffset bypasses applyScrollAdjustment's isIOSWebKit() logic and
can cancel native iOS momentum; instead of calling _scrollToOffset directly when
key !== null && !followOnAppend, route the synchronization through
applyScrollAdjustment (or its equivalent helper) so the iOS-specific
deferred-scroll gate is respected; use this.getScrollOffset() as the target
offset and pass through the adjustments/behavior params (or undefined) to
applyScrollAdjustment so iOS WebKit will defer the DOM write while other
platforms still sync immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 604-615: The code may read stale measurement data when computing
newOffset via newMeasurements[idx].start; before using newMeasurements[idx]
ensure measurements are rebuilt for the current key ordering: either call your
measurement invalidation/rebuild helper (e.g., this.invalidateMeasurements() or
this.rebuildMeasurements()) or include the item key identity in the measurement
memo so getMeasurements() returns a fresh array for the current getItemKey
order; specifically, after computing idx using getItemKey and before accessing
newMeasurements[idx].start, force a measurements refresh (or verify
newMeasurements[idx] corresponds to getItemKey(idx) and rebuild if it does not)
so this.scrollOffset is computed from up-to-date measurements.

---

Duplicate comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 830-838: The direct call to this._scrollToOffset bypasses
applyScrollAdjustment's isIOSWebKit() logic and can cancel native iOS momentum;
instead of calling _scrollToOffset directly when key !== null &&
!followOnAppend, route the synchronization through applyScrollAdjustment (or its
equivalent helper) so the iOS-specific deferred-scroll gate is respected; use
this.getScrollOffset() as the target offset and pass through the
adjustments/behavior params (or undefined) to applyScrollAdjustment so iOS
WebKit will defer the DOM write while other platforms still sync immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fad320ff-17ef-4872-9c13-0986c5a892c7

📥 Commits

Reviewing files that changed from the base of the PR and between 5b972c5 and 2b35365.

📒 Files selected for processing (3)
  • .changeset/eager-scroll-anchor.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/eager-scroll-anchor.md

Comment thread packages/virtual-core/src/index.ts
@piecyk piecyk force-pushed the damian/fix/eager-scroll-anchor-on-prepend branch 4 times, most recently from 38779af to 4d28f87 Compare June 1, 2026 16:56
…jump

When items are prepended with anchorTo: 'end' and dynamic sizes,
the virtualizer would compute the wrong visible range for one frame
(using stale estimate-based positions) and then correct in the next
frame via _willUpdate, producing a visible "jump".

Fix by eagerly adjusting scrollOffset in setOptions during the render
pass so calculateRange/getVirtualItems return the correct items
immediately. _willUpdate then only syncs the browser's actual scroll
position to match.
@piecyk piecyk force-pushed the damian/fix/eager-scroll-anchor-on-prepend branch from 4d28f87 to 8a58d32 Compare June 1, 2026 17:05
@tannerlinsley tannerlinsley merged commit c746841 into TanStack:main Jun 1, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants