Skip to content

fix(virtual-core): keep a travelling smooth scroll alive through a prepend - #1248

Merged
piecyk merged 4 commits into
TanStack:mainfrom
piecyk:fix/anchor-clobbered-by-stale-scroll-state
Sep 11, 2026
Merged

piecyk merged 4 commits into
TanStack:mainfrom
piecyk:fix/anchor-clobbered-by-stale-scroll-state

Conversation

@piecyk

@piecyk piecyk commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

With anchorTo: 'end', a smooth scrollToIndex that is still travelling when history is prepended dies partway. "Jump to the oldest message" strands the reader mid-list whenever older messages stream in during the animation.

🎯 Changes

Root cause. The prepend anchor sync in _willUpdate writes scrollTop instantly to keep the reader's row in place. That write cancels the browser's in-flight smooth animation. reconcileScroll cannot recover it: it only re-drives when the target moves, and with an index-based target the destination is unchanged. Re-driving unconditionally does not help either. Instrumented runs show Chromium silently drops a smooth request issued in the frame right after an instant write cancelled its animation, while the same request issued later completes.

Fix. Skip the anchor DOM write while a smooth programmatic scroll is still travelling. The target recomputes against the new layout on every reconcile frame, so the animation simply continues; the next scroll event re-syncs the tracked offset. A smooth scroll that has already landed but not yet been retired still receives the sync, so a reader's position is preserved in that window.

Tests.

  • Two core regression tests: the travelling case fails on main, the landed case guards the refinement.
  • The react-virtual smooth-prepend e2e page and spec, which reproduce the strand in a real browser, promoted from test.fail() to a regular guard.

Verified locally: virtual-core 164/164, react e2e 37/37 (the new spec 3/3 on repeat), marko chat/chat-pretext/option-gates 26 passed, angular e2e 14/14, tsc/eslint/prettier clean.

Not in scope. A finished programmatic scroll can stay live for up to the 5 s reconcile safety valve if the user scrolls away before it is retired; a prepend inside that window now skips the sync instead of re-firing the old target. Retiring stalled scrolls early would risk dynamic-size scrollToIndex, which relies on the loop re-asserting as measurements arrive, so it is left for a follow-up.

✅ 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).

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

This change adds a smooth-prepend virtualized list example, registers it in the Vite build, and adds a Playwright regression test for scrolling interrupted by prepended history.

Changes

Smooth prepend end-to-end scenario

Layer / File(s) Summary
Smooth prepend example
packages/react-virtual/e2e/app/smooth-prepend/index.html, packages/react-virtual/e2e/app/smooth-prepend/main.tsx, packages/react-virtual/e2e/app/vite.config.ts
The new app renders 200 virtualized messages, starts at the list end, supports smooth scrolling and prepending five messages, and is added to the Vite inputs.
Smooth prepend regression coverage
packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts
The Playwright test checks the initial bottom position, interrupts smooth scrolling by prepending history, and verifies that scrolling reaches the top. The test records the current behavior as an expected failure.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise, specific, and describes the primary fix for preserving an in-flight smooth scroll during a prepend.
Description check ✅ Passed The description is complete and relevant. It explains the root cause, fix, tests, scope limitations, checklist status, and release impact.
✨ 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.

@nx-cloud

nx-cloud Bot commented Jul 31, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit ba2c744

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

☁️ Nx Cloud last updated this comment at 2026-09-11 15:56:28 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

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

@tanstack/lit-virtual

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

@tanstack/marko-virtual

npm i https://pkg.pr.new/@tanstack/marko-virtual@1248

@tanstack/react-virtual

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

@tanstack/solid-virtual

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

@tanstack/svelte-virtual

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

@tanstack/virtual-core

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

@tanstack/vue-virtual

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

commit: 032f913

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 670-684: The anchorResolved handling must not clear an active
smooth scroll. Update the scroll-state retirement logic near reconcileScroll so
anchor correction only retires stale programmatic targets after the destination
has been reached and the user has moved away, while preserving in-flight
scrollToIndex({ behavior: "smooth" }) state through prepend/setOptions updates;
add regression coverage for prepending during an active smooth scroll.
🪄 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 Plus

Run ID: 28b4746c-99bc-4c31-84d9-5e8c279082c0

📥 Commits

Reviewing files that changed from the base of the PR and between d2cf98b and 4bf3ab9.

📒 Files selected for processing (3)
  • .changeset/lucky-pandas-listen.md
  • 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 fix/anchor-clobbered-by-stale-scroll-state branch from 4bf3ab9 to 709d666 Compare July 31, 2026 10:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 1132-1142: Update the reachedTarget re-arm logic in the scroll
target handling block to use the same approximately 1.01 measurement-settling
tolerance as the retirement check, rather than the viewport distance. Re-arm
this.scrollState.reachedTarget whenever the target changes beyond that
tolerance, including changes smaller than a viewport but larger than the
settling threshold; preserve the existing behavior for minor measurement nudges.
🪄 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 Plus

Run ID: ae39a292-8f42-4251-a538-6cd0c1e82b54

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf3ab9 and 709d666.

📒 Files selected for processing (3)
  • .changeset/lucky-pandas-listen.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/lucky-pandas-listen.md

Comment thread packages/virtual-core/src/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/react-virtual/e2e/app/smooth-prepend/main.tsx`:
- Line 34: Remove the unnecessary non-null assertions from the array accesses in
the getItemKey callback and the corresponding access at the second reported
location, preserving their existing indexing and key behavior.

In `@packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts`:
- Around line 42-45: Remove the top-level test.fail() call before the
smooth-scroll test declaration; leave the test('a prepend mid-flight does not
abandon a smooth scrollToIndex', ...) registration and body unchanged.
🪄 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 Plus

Run ID: 512dd6ae-228e-488e-95e4-7b06c3040ef4

📥 Commits

Reviewing files that changed from the base of the PR and between 709d666 and 88c894a.

📒 Files selected for processing (4)
  • packages/react-virtual/e2e/app/smooth-prepend/index.html
  • packages/react-virtual/e2e/app/smooth-prepend/main.tsx
  • packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts
  • packages/react-virtual/e2e/app/vite.config.ts

count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
getItemKey: (index) => messages[index]!.id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unnecessary non-null assertions.

ESLint reports errors at Line 34 and Line 88. Remove ! from both array accesses.

Proposed fix
-    getItemKey: (index) => messages[index]!.id,
+    getItemKey: (index) => messages[index].id,
...
-            const message = messages[item.index]!
+            const message = messages[item.index]

Also applies to: 88-88

🧰 Tools
🪛 ESLint

[error] 34-34: This assertion is unnecessary since it does not change the type of the expression.

(@typescript-eslint/no-unnecessary-type-assertion)

🤖 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/react-virtual/e2e/app/smooth-prepend/main.tsx` at line 34, Remove
the unnecessary non-null assertions from the array accesses in the getItemKey
callback and the corresponding access at the second reported location,
preserving their existing indexing and key behavior.

Source: Linters/SAST tools

Comment thread packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts Outdated
@piecyk
piecyk force-pushed the fix/anchor-clobbered-by-stale-scroll-state branch from 9f9632b to b96bba2 Compare July 31, 2026 12:58
piecyk and others added 2 commits September 11, 2026 17:09
…epend

With anchorTo: 'end', the prepend anchor sync in _willUpdate writes
scrollTop instantly. That cancels a smooth scrollToIndex the browser is
still animating, and Chromium drops a smooth request re-issued in the
frame right after such a cancel, so re-driving from reconcileScroll
cannot recover it: "jump to the oldest message" died halfway whenever
history streamed in mid-animation.

Skip the anchor DOM write while a smooth programmatic scroll is still
travelling. Its target is index-based and recomputes against the new
layout every reconcile frame, so the animation simply continues; the
next scroll event re-syncs the tracked offset. A smooth scroll that has
already landed but has not been retired yet still receives the sync, so
a reader's position is preserved in that window.

Two core regression tests cover both branches, and the react-virtual
smooth-prepend e2e is promoted from an expected failure to a regular
guard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@piecyk piecyk changed the title fix(virtual-core): don't let a stale scroll target clobber the prepend anchor fix(virtual-core): keep a travelling smooth scroll alive through a prepend Sep 11, 2026
@piecyk
piecyk merged commit 2c0a0ea into TanStack:main Sep 11, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 11, 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.

1 participant