Public API to cancel or retarget the scroll reconcile loop armed by scrollTo*/scrollBy
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.1k
- Forks
- 466
- Avg merge
- 2d 31m
- Merged PRs (30d)
- 13
Description
Summary
Since virtual-core 3.17, every call to scrollToOffset, scrollToIndex, scrollBy, or scrollToEnd arms a rAF reconcile loop (scheduleScrollReconcile → reconcileScroll) that keeps re-asserting the estimate-derived target offset until the actual scroll offset converges (or ~5000ms elapse). scrollState and rafId are private; there is no public way to cancel or retarget the loop once armed.
This makes a common coarse-then-fine correction pattern impossible: after scrollToIndex(i, { align: 'start' }), an app that reads the true DOM rect on the next frame and issues a corrective delta will have that correction reverted on the loop's next tick — the loop's target was computed from stale estimated sizes, so it believes it hasn't converged.
On iOS specifically, any library-issued scroll write kills active momentum scrolling, so even a single re-assertion visibly jars the user.
Motivation / current workaround
We maintain a chat timeline (a Cinny fork) with dynamic row measurement and content-based estimates. When restoring a scroll anchor after prepending history, the natural flow is:
- Coarse:
virtualizer.scrollToIndex(anchorIndex, { align: 'start' })to mount the region. - Next frame: read the anchor element's
getBoundingClientRect(), compute the true pixel delta,scrollBy(delta).
Under 3.17 the reconcile loop reverts step 2 for up to 5 seconds. We had to bypass the library for these writes:
// Direct write, NOT virtualizer.scrollToOffset: since virtual-core 3.17 every
// scrollTo* call arms a rAF reconcile loop that keeps re-asserting its own
// (estimate-derived, so wrong) target offset until the actual offset converges
// — which reverts the DOM-rect delta correction on its next frame. The library
// offers no way to cancel or retarget that loop, so this path must bypass it.
scrollElement.scrollTo({ top: offset, behavior: 'instant' });
Bypassing works, but it means re-deriving the virtualizer's offset semantics by hand (getOffsetForIndex + scrollMargin conversions), which is fragile across library upgrades.
Minimal repro sketch
const virtualizer = useVirtualizer({
count: 1000,
getScrollElement: () => ref.current,
estimateSize: () => 50, // real rows are ~120px
});
virtualizer.scrollToIndex(500, { align: 'start' });
requestAnimationFrame(() => {
const row = ref.current!.querySelector('[data-index="500"]')!;
const delta =
row.getBoundingClientRect().top - ref.current!.getBoundingClientRect().top;
ref.current!.scrollBy({ top: delta, behavior: 'instant' });
// Correction lands; next frame reconcileScroll re-asserts the
// estimate-derived offset and row 500 is no longer aligned.
});
Proposed API
Preferred: a public cancel method.
cancelScroll(): void {
if (this.rafId != null) {
this.targetWindow?.cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.scrollState = null;
}
Alternatives:
- Per-call opt-out:
scrollToIndex(i, { align: 'start', reconcile: false })(and equivalents). - Retarget:
virtualizer.retargetScroll(offset)to replace the loop's target without re-arming.
Any of these would let us go back through the virtualizer instead of bypassing it.
Environment
@tanstack/virtual-core3.17.3 (@tanstack/react-virtual)- React 18, Vite, Vitest; reproduced on Chromium and iOS Safari
Happy to open a PR if an API shape is agreed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the scrollToOffset, scrollToIndex, scrollBy, and scrollToEnd entry points through scheduleScrollReconcile and reconcileScroll, including how scrollState and rafId are updated. Define the public cancellation, opt-out, or retargeting behavior so the caller's corrective scroll is not reverted, while preserving the existing convergence behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100