fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas by piecyk · Pull Request #1235 · TanStack/virtual · GitHub
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

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

@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

@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