Skip to content

fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas#1235

Merged
piecyk merged 2 commits into
TanStack:mainfrom
piecyk:fix/ios-deferral-stale-replay
Jul 20, 2026
Merged

fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas#1235
piecyk merged 2 commits into
TanStack:mainfrom
piecyk:fix/ios-deferral-stale-replay

Conversation

@piecyk

@piecyk piecyk commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes both manifestations of #1233. On iOS WebKit the end-anchored virtualizer defers scroll compensation while it considers the scroller live, accumulating deltas in _iosDeferredAdjustment and replaying them in _flushIosDeferredIfReady() once things settle. Both bugs are the deferral replaying a delta whose premise no longer holds by flush time.

Fix 1 — absolute scroll commands invalidate the pending deferral

scrollToOffset / scrollToIndex (and therefore scrollToEnd) compute their target from current measurements, so any deferred delta pending at that moment is stale — the command already accounts for the measurements the delta was compensating for. Previously the delta survived the command and replayed on the next flush (~150 ms later, at isScrollingResetDelay), shifting the list off the just-established position.

Reported symptom: programmatically restoring a saved reading position (repeated scrollToOffset while rows measure in) lands pixel-correct, then the flush fires and shifts the list a constant +74 px.

Fix: drop the deferral as the first statement of both absolute commands. Relative commands (scrollBy) keep it, since they build on the current offset. Off-iOS this is a no-op — the field is always 0 there.

Fix 2 — the flush drops a negative delta the browser's end clamp already absorbed

With anchorTo: 'end' and the reader pinned at the bottom, a row above the viewport re-measuring smaller during isScrolling shrinks maxScrollOffset; the browser clamps scrollTop onto the new bottom, which is already the correct end-anchored position. The library had separately deferred a negative compensation for that same shrink, and the flush replayed it on top of the clamped position, lifting the view off the bottom by the net delta.

Reported symptom: a single room-open accumulated twelve deltas summing to −101 while the list settled at the bottom; the flush then wrote scrollTop 3543 → 3442, 101 px above the bottom.

Fix: the flush recognizes the end clamp (cur >= max - 1) and drops the stale negative delta. Positive deltas still replay — content growth above the viewport does not clamp (the browser can't shrink to fit growth) and the consumer's DOM sizer may not have grown yet, so a positive delta is still needed.

Testing

  • iOS deferral: an absolute scroll command invalidates a pending deferred adjustment (Fix 1, both commands).
  • iOS deferral: a negative delta at the end clamp is dropped, not replayed (Fix 2).
  • iOS deferral: a positive delta at the end clamp still replays (growth above does not clamp) (guards against over-correcting Fix 2).
  • Full virtual-core suite passes (122/122); typecheck and eslint clean.

Both fixes are device-verified by the reporter (@stevan-borus) in production — the constant +74 px restore drift and the −101 px bottom jump are both gone.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented stale iOS scroll adjustments from replaying after users issue absolute scroll commands.
    • Improved end-anchored scrolling near the bottom of a list by avoiding compensations that could move the view away from the correct position.
    • Preserved valid positive adjustments during end-anchored scrolling.
  • Documentation

    • Clarified iOS deferred-scrolling behavior for absolute and relative scroll commands.

…ute scroll commands

On iOS WebKit, scrollToOffset/scrollToIndex (and thus scrollToEnd) derive
their target from current measurements, so any pending iOS-deferred
compensation is stale by definition. Previously the deferral survived the
command and replayed once isScrolling reset (~150 ms later), shifting an
anchorTo: 'end' list off the just-established position by the accumulated
delta. Drop the deferral at the start of both absolute commands; relative
commands (scrollBy) keep it.

Fixes manifestation A of TanStack#1233.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

iOS scroll deferral handling

Layer / File(s) Summary
Update iOS deferral handling
packages/virtual-core/src/index.ts
Absolute scroll commands clear pending iOS adjustments, while end-clamped negative adjustments are discarded and positive adjustments remain replayable.
Validate behavior and record patch release
packages/virtual-core/tests/index.test.ts, .changeset/ios-deferral-stale-replay.md
Tests cover absolute-scroll invalidation and end-clamp behavior; the changeset documents the iOS behavior updates.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • TanStack/virtual#1220 — Also clears stale _iosDeferredAdjustment state in packages/virtual-core/src/index.ts.

Suggested reviewers: 2wheeh, tannerlinsley

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers the fixes and testing, but it omits the required checklist and release-impact sections from the template. Add the template's Checklist and Release Impact sections, and explicitly mark the relevant items or explain why they don't apply.
✅ Passed checks (4 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 and accurately summarizes the main iOS deferred-scroll fix.
✨ 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 20, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 4457f3d

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

☁️ Nx Cloud last updated this comment at 2026-07-20 08:20:43 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

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

@tanstack/lit-virtual

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

@tanstack/marko-virtual

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

@tanstack/react-virtual

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

@tanstack/solid-virtual

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

@tanstack/svelte-virtual

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

@tanstack/virtual-core

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

@tanstack/vue-virtual

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

commit: 9ce8a1f

@piecyk piecyk changed the title fix(virtual-core): invalidate iOS deferred scroll adjustment on absolute scroll commands fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas (#1233) Jul 20, 2026
…lamp

With anchorTo: 'end' on iOS WebKit, when a row above the viewport re-measures
smaller while the reader is pinned at the bottom, the browser shrinks
maxScrollOffset and clamps scrollTop onto the new bottom — already the correct
end-anchored position. The library separately deferred a negative compensation
for the same shrink, and _flushIosDeferredIfReady replayed it on top of the
clamped position, lifting the view off the bottom.

The flush now recognizes the end clamp (cur >= max - 1) and drops the stale
negative delta. Positive deltas still replay: growth above the viewport does
not clamp, and the consumer's DOM sizer may not have grown yet.

Fixes manifestation B of TanStack#1233.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@piecyk
piecyk force-pushed the fix/ios-deferral-stale-replay branch from e3b982f to 9ce8a1f Compare July 20, 2026 08:16
@piecyk piecyk changed the title fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas (#1233) fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas Jul 20, 2026
@piecyk
piecyk merged commit 7dcfc07 into TanStack:main Jul 20, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 20, 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