Skip to content

fix(virtual-core): request deferred ResizeObserver frames from the scroll element's window - #1320

Merged
piecyk merged 1 commit into
TanStack:mainfrom
NotYoojun:fix/raf-target-window
Oct 13, 2026
Merged

piecyk merged 1 commit into
TanStack:mainfrom
NotYoojun:fix/raf-target-window

Conversation

@NotYoojun

@NotYoojun NotYoojun commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🎯 Changes

With useAnimationFrameWithResizeObserver: true, both ResizeObserver callbacks defer their work with the global requestAnimationFrame:

  • observeElementRect, for the scroll element's size
  • the item observer behind measureElement

All other window-bound APIs in the virtualizer go through instance.targetWindow (scrollElement.ownerDocument.defaultView): ResizeObserver, timers, performance.now(), and the scrollToIndex reconcile frame. These two calls were the only exceptions.

Problem 1: frames come from the wrong window. A same-origin popup opened with window.open() shares the opener's JS realm, so the global requestAnimationFrame belongs to the opener. When a list is rendered into the popup (for example with createPortal), its deferred measurements wait on the opener's frames. Browsers stop those frames while the opener is minimized or fully occluded, so row measurements and the viewport size in the popup stop updating until the opener is visible again.

Problem 2: queued frames are never cancelled. Suppose the scroll element changes while a frame is pending, for example when a panel moves into a popup and the virtualizer instance survives. The old element's rect is then applied after the new element's initial rect and overwrites scrollRect. Item entries from a disconnected observer are still processed in the same way.

Fix. A small internal createFrameQueue(targetWindow) requests frames from the scroll element's window and tracks their ids. observeElementRect cancels its pending frames when it unsubscribes. The item observer cancels them in disconnect(), which cleanup() calls.

With the option off (the default), nothing changes. With it on and the list in the main window, the only difference is that pending frames are cancelled on teardown.

Tests. Three unit tests in packages/virtual-core/tests/index.test.ts use a mock window separate from the global one. All three fail on main and pass with this change:

  • a deferred item measurement is scheduled on the scroll element's window, not the global one, and is applied when that frame runs
  • cleanup cancels a deferred item measurement still waiting for its frame
  • observeElementRect defers to the scroll element's window and cancels the pending frame on unsubscribe

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Animation-frame callbacks now use the scroll element’s window, improving behavior when scrolling elements belong to a different window.
    • Pending callbacks are canceled when observation ends, preventing measurements or updates from running after cleanup.

Copilot AI balanced review requested due to automatic review settings October 13, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@changeset-bot

changeset-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a51a749

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@tanstack/virtual-core Patch
@tanstack/angular-virtual Patch
@tanstack/lit-virtual Patch
@tanstack/marko-virtual Patch
@tanstack/react-virtual Patch
@tanstack/solid-virtual Patch
@tanstack/svelte-virtual Patch
@tanstack/vue-virtual Patch
@tanstack/virtual-benchmarks Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fa23efc8-bc65-4696-ab31-a07947eb34fc
📥 Commits

Reviewing files that changed from the base of the PR and between 676a098 and a51a749.

📒 Files selected for processing (3)
  • .changeset/raf-scroll-element-window.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

ResizeObserver callbacks now use the scroll element’s window for animation-frame scheduling. Cleanup cancels queued frames for element-rect observation and item measurements.

Changes: Resize observation scheduling

Layer / File(s) Summary
Frame queue and element-rect observation
packages/virtual-core/src/index.ts, packages/virtual-core/tests/index.test.ts, .changeset/raf-scroll-element-window.md
A frame queue schedules callbacks through a supplied window and cancels pending frames. observeElementRect uses the target window’s queue and cancels queued callbacks on unsubscribe. Tests verify scheduling and unsubscribe behavior. The changeset records the patch.
Item ResizeObserver scheduling
packages/virtual-core/src/index.ts, packages/virtual-core/tests/index.test.ts
The item ResizeObserver schedules deferred measurements through the target window and cancels queued frames on disconnect. Tests verify deferred measurements and cancellation during unmount.

Priority: ⬇️ Low

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

Change: Bug fix

Suggested reviewers: piecyk

Merge Risk: ⚪ Minimal · up to a51a7

No concrete issue remains that needs resolution before merging.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the primary change: using the scroll element's window for deferred ResizeObserver frames.
Description check Passed The description explains the problem, fix, tests, and checklist status. The changeset exists, but the published-code checkbox should be selected because the change affects @tanstack/virtual-core.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/virtual-core/tests/index.test.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): packages/virtual-core/tests/index.test.ts

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.

@piecyk piecyk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks good. The three new tests fail on main and pass here. The cancel-on-cleanup path is safe because _willUpdate re-observes cached elements after cleanup, so an initial ResizeObserver entry re-delivers any dropped measurement.

Out of curiosity, what made you reach for useAnimationFrameWithResizeObserver in the popup case? Were you hitting the "ResizeObserver loop completed with undelivered notifications" error, or something else?

The option delays every measurement by a frame, so I'd like to understand the real-world reason people turn it on. If it's the loop error, we might be able to fix that at the source so the option isn't needed.

@nx-cloud

nx-cloud Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit a51a749

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

☁️ Nx Cloud last updated this comment at 2026-10-13 12:58:00 UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 13, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

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

@tanstack/lit-virtual

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

@tanstack/marko-virtual

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

@tanstack/react-virtual

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

@tanstack/solid-virtual

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

@tanstack/svelte-virtual

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

@tanstack/virtual-core

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

@tanstack/vue-virtual

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

commit: a51a749

@NotYoojun

Copy link
Copy Markdown
Contributor Author

Out of curiosity, what made you reach for useAnimationFrameWithResizeObserver in the popup case? Were you hitting the "ResizeObserver loop completed with undelivered notifications" error, or something else?

The option delays every measurement by a frame, so I'd like to understand the real-world reason people turn it on. If it's the loop error, we might be able to fix that at the source so the option isn't needed.

Thanks for checking!

Some context on why we care: our app is multi-window by design (both in the browser and in Electron). It opens child windows with window.open('about:blank') and mounts content into them with React portals or direct DOM operations, so all windows share a single JS thread and realm. This setup is common and very crucial in industrial software and professional editors, and those child windows may host virtualized lists, reports and DataGrid.

Routing everything through the scroll element's window is a big part of why TanStack Virtual fits us, so we audited the core for anything still tied to the global window. We don't enable useAnimationFrameWithResizeObserver in most of the codebase, and we haven't hit the loop error. It's more like a robustness check for us and these two calls were the last global-window dependencies we found, and we wanted the option to work in a child window for anyone who does turn it on.

If you end up fixing the loop error at the source and retiring the option, that works for us too, since these calls would go away with it.

@piecyk

piecyk commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

@NotYoojun Thanks for the context, that makes sense. Nice to hear the window routing is what drew you in; keeping everything on the scroll element's window is exactly the intent. Merging this, and I'll keep the option-retirement idea on the list.

@piecyk
piecyk merged commit 269e8ed into TanStack:main Oct 13, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 13, 2026
@NotYoojun
NotYoojun deleted the fix/raf-target-window branch October 14, 2026 07:34
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.

3 participants