fix(virtual-core): stop iOS deferred scroll adjustments from replaying stale deltas#1235
Merged
Merged
Conversation
…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>
📝 WalkthroughWalkthroughChangesiOS scroll deferral handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
View your CI Pipeline Execution ↗ for commit 4457f3d
☁️ Nx Cloud last updated this comment at |
…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
force-pushed
the
fix/ios-deferral-stale-replay
branch
from
July 20, 2026 08:16
e3b982f to
9ce8a1f
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
_iosDeferredAdjustmentand 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 thereforescrollToEnd) 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, atisScrollingResetDelay), shifting the list off the just-established position.Reported symptom: programmatically restoring a saved reading position (repeated
scrollToOffsetwhile 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 always0there.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 duringisScrollingshrinksmaxScrollOffset; the browser clampsscrollToponto 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).virtual-coresuite 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
Documentation