Skip to content

With useScrollendEvent, scrollToIndex and scrollToEnd stop short when the target moves while they settle #1375

Open

Description

@Darkatse

Describe the bug

With useScrollendEvent: true, a scrollToIndex or scrollToEnd can stop before its target with no user input. It happens when the target moves while the command is settling. I found two ways:

  1. In one ResizeObserver batch, a row above the viewport grows (the virtualizer corrects the scroll position for it) and a row in the viewport above the target grows too (no correction). The target moves further than the correction, and the command ends where the correction left the view.
  2. Rows above the target shrink before the command's write is reported back (fix(virtual-core): let a user scroll end a non-smooth scrollToIndex before it lands #1310). The browser clamps the write at the smaller max, and the command ends there.

With the default useScrollendEvent: false, both land on the target.

Your minimal, reproducible example

Live: https://virtual-scrollend-repro.darkatse.workers.dev
Source: https://github.com/Darkatse/virtual-scrollend-repro

It shows case 1 with the published @tanstack/react-virtual 3.14.26, pinned. All the code is in src/main.tsx, and it depends only on React.

Steps to reproduce

  1. Open the page and click Run. It calls scrollToIndex(60, { align: 'center' }). In the frame after its scroll event, row 55 grows to 100px and row 58 to 150px.
  2. Click run with false (useScrollendEvent: false), then click Run again.

Case 2: the index-shrink button on the command-retire e2e page, with useScrollendEvent: true on its virtualizer, ends at 4000 instead of 3900 in Chromium. WebKit fires no scrollend there.

Expected behavior

Both runs end with row 60 centred on the dashed line (scrollTop 2975).

Actual behavior

With useScrollendEvent: true the run ends at scrollTop 2875, 100px short, and the command has ended. With false it lands on 2975.

How often does this bug happen?

Every time

Platform

  • macOS 27.2, Chromium 156.0.8078.4 and WebKit 26.0 (Playwright builds); the same result in both.
  • Also seen in a chat app on Android WebView and iOS.

tanstack-virtual version

@tanstack/react-virtual 3.14.26 (@tanstack/virtual-core 3.19.11). Present since virtual-core 3.18.0 (scroll events retire commands, #1294) through main.

TypeScript version

5.9.3

Additional context

Each scroll write is reported a second time when the element comes to rest, and the virtualizer reads that second report as an external scroll, as if the user had scrolled away. With useScrollendEvent it is a scrollend at the same position in the same frame, so it arrives before reconcile has written the moved target. externalRetiresCommand sees the element neither where the command last wrote nor at the fresh target, and ends the command. With the debounced fallback, the second report comes after reconcile.

I have a fix and will open a PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions