Skip to content

fix(react-virtual): position directDomUpdates rows that mount without the owner - #1301

Merged
piecyk merged 1 commit into
TanStack:mainfrom
piecyk:damian/fix/direct-dom-row-positioning
Oct 9, 2026
Merged

piecyk merged 1 commit into
TanStack:mainfrom
piecyk:damian/fix/direct-dom-row-positioning

Conversation

@piecyk

@piecyk piecyk commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

With directDomUpdates, rows were positioned only by the owner's layout effect or by the next onChange. A row mounted by a child that re-renders on its own (local state, context, a resolved Suspense boundary) commits without the owner, so that effect never runs. A fixed-size row raises no onChange when measured either, so the row stays unpositioned until the range changes.

Rows are now positioned as they register:

 measureElement(node)                  // ref callback wrapper
   virtualizer.measureElement(node)
+  if directDomUpdates && container
+    applyItemPosition(item, node)     // skipped by lastPositions if already placed

 containerRef(node)
-  write container size
+  applyDirectStyles()                 // size + rows whose refs attached before the container's

 applyDirectStyles()
   write container size
   for each virtual item with an element
-    inline position write
+    applyItemPosition(item, el)

applyItemPosition is the per-row write pulled out of applyDirectStyles. It is unchanged and stays idempotent through lastPositions.

Extracted from #1296: useVirtualizerState makes this case common because it lets a memoised child render rows on its own, but the bug exists without it.

Evidence

Two new tests in packages/react-virtual/tests/index.test.tsx. Both use fixed 50px rows and directDomUpdates:

child (memo, own state) reveals row 2 after mount
  expect row 1 transform = translate3d(0, 50px, 0)    // owner-committed row
  expect row 2 transform = translate3d(0, 100px, 0)

child (memo, own state) mounts container + rows after mount
  expect row 3 transform = translate3d(0, 150px, 0)
  • Before (on main): both fail. Row 1 is positioned, but the rows the child mounts later have no transform.
    × directDomUpdates positions a row a child mounts without the owner
    × directDomUpdates positions rows a child mounts together with the container
    Tests  2 failed | 8 passed (10)
    
  • After:
    Tests  10 passed (10)
    

Also run for react-virtual: test:types, test:eslint, build, and the full e2e suite (36 passed).

Merge Danger

Door: two-way

No API change. The fix only adds position writes in places that skipped them before, and reverting it is a plain revert.

Blast Radius: directDomUpdates

Only affects directDomUpdates: true. A row can now get its position one step earlier, from its own ref callback instead of the owner's layout effect. The value written is the same, and lastPositions stops the effect from writing it twice.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr. (I ran the affected react-virtual targets listed above instead.)

🚀 Release Impact

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

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Rows using direct DOM updates are now positioned correctly as soon as they mount, including when they appear independently of their parent or at the same time as the scrolling container.
    • Rows registered for measurement are also positioned immediately when the container is available, preventing them from briefly appearing in the wrong location.

… the owner

A row mounted by a child that re-renders on its own (local state, context,
a resolved Suspense boundary) never reached the owner's applyDirectStyles
layout effect, and a fixed-size row raises no onChange when measured, so it
stayed unpositioned until the range changed. Position rows as they register
through measureElement, and let containerRef position the rows that mounted
together with the container, since their refs attach before its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e83d3c3

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

This PR includes changesets to release 2 packages
Name Type
@tanstack/react-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 9, 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: 323864c5-dce2-4d9e-aa08-744c56894eaa

📥 Commits

Reviewing files that changed from the base of the PR and between 43675bc and e83d3c3.


📒 Files selected for processing (3)
  • .changeset/direct-dom-row-positioning.md
  • packages/react-virtual/src/index.tsx
  • packages/react-virtual/tests/index.test.tsx

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

Walkthrough

The React adapter now positions virtual rows during measurement registration and when a container attaches. Tests cover rows revealed by an independent child update and rows mounted with the container.

Changes

Direct DOM row positioning

Layer / File(s) Summary
Row positioning and mount handling
packages/react-virtual/src/index.tsx, packages/react-virtual/tests/index.test.tsx, .changeset/direct-dom-row-positioning.md
A shared helper applies item offsets using transforms or top/left and skips unchanged offsets. Measurement registration positions new items when direct updates are enabled and a container exists. Container attachment applies direct styles. Tests cover both mounting scenarios, and a patch changeset records the change.

Priority: ⬇️ Low

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

Change: Bug fix


Merge Risk: ⚪ Minimal · up to e83d3

No merge-blocking issue was identified in the row-positioning change.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary fix: positioning directDomUpdates rows that mount without the owner.
Description check Passed The description explains the cause, implementation, scope, test evidence, checklist status, and release impact. It uses ## Summary instead of the template's ## 🎯 Changes heading, but it provides t…
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 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 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/react-virtual/tests/index.test.tsx

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




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.

@nx-cloud

nx-cloud Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit e83d3c3

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

☁️ Nx Cloud last updated this comment at 2026-10-09 14:05:32 UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 9, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

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

@tanstack/lit-virtual

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

@tanstack/marko-virtual

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

@tanstack/react-virtual

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

@tanstack/solid-virtual

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

@tanstack/svelte-virtual

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

@tanstack/virtual-core

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

@tanstack/vue-virtual

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

commit: e83d3c3

@piecyk
piecyk merged commit ce57b78 into TanStack:main Oct 9, 2026
10 checks passed
piecyk added a commit to piecyk/virtual that referenced this pull request Oct 9, 2026
…irtualizerState

The positioning fix itself landed in TanStack#1301. These tests cover the case
the hook makes common: a memoised child rendering rows through
`useVirtualizerState` commits on its own when the owner, or anything in
its render pass, has already read the new range. `_willUpdate` then
publishes to the child without an `onChange`, the owner does not render
again, and the child's rows mount after the owner's layout effect has
run.

Also pins `onChange` in the `_willUpdate` publish tests: it fires once,
before the listener, for a range change, and not at all when only the
total size changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Oct 9, 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