Skip to content

fix(react-virtual): skip flushSync while measuring from the ref callback - #1282

Open
Jigoooo wants to merge 1 commit into
TanStack:mainfrom
Jigoooo:fix/react-virtual-flushsync-during-commit
Open

fix(react-virtual): skip flushSync while measuring from the ref callback#1282
Jigoooo wants to merge 1 commit into
TanStack:mainfrom
Jigoooo:fix/react-virtual-flushsync-during-commit

Conversation

@Jigoooo

@Jigoooo Jigoooo commented Sep 13, 2026

Copy link
Copy Markdown

🎯 Changes

Fixes #1281.

Problem

measureElement is handed to item elements as a ref, so React calls it during the commit phase. When the measured size differs from estimateSize for an item above the current scroll offset, resizeItem compensates the scroll position and raises a sync notify — and this adapter then calls flushSync(rerender) while React is still committing. React runs the callback but cannot flush, so development builds log

flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. …

once per measured item (21 on first mount and 11 per list switch in the app measured below). Previously reported in #628 and #1094.

Fix

Mark the window in which the virtualizer measures from its ref callback, and skip flushSync only for notifies raised inside it:

  • wrap instance.measureElement once, where the instance is created, to set and clear measuringFromRef
  • onChange keeps flushSync for every other sync notify — ResizeObserver re-measures and scroll adjustments arrive outside the commit phase and still flush synchronously

Why this keeps behaviour identical. React's flushSync sets Internals.p = 2 (DiscreteEventPriority) and runs the callback; while committing it only skips the flush (Internals.d.f()flushSyncWork() returns true, which is what produces the warning). The commit phase itself already runs at DiscreteEventPriority, so an update scheduled from there lands in the same lane and is flushed at the same point with or without flushSync.

useFlushSync: false, the workaround suggested on the earlier issues, is broader: it also drops the ResizeObserver path to default priority, so rows that grow after mount (late-loading images) settle a frame later.

Test verification (RED -> GREEN)

New test in packages/react-virtual/tests/index.test.tsx. The existing List helper gained an initialOffset prop so that measured items sit above the fold:

render(<List itemSize={100} dynamic initialOffset={400} />)
  • RED — with the source change reverted, the test fails: console.error receives the flushSync warning 3 times.
  • GREEN — with the fix, packages/react-virtual passes 8/8 (7 existing + the new one).

Browser verification in a real app

React 19.2.8, @tanstack/react-virtual 3.14.12 + virtual-core 3.17.10, headless Chromium. One virtualized chat timeline mounted in isolation (80 rows, anchorTo: 'end', estimateSize 56 vs real 90–300), three phases: first mount → switch to another list → switch back. Three runs of each build; runs of the same build were frame-identical, so the comparison is not noise.

before after
flushSync warnings 21 / 11 / 11 0 / 0 / 0
commits containing the list (React.Profiler) 7 / 7 / 6 7 / 7 / 6
per-frame scrollTop + row transform, 40 frames per phase identical, 120 / 120 frames

The same change (applied to the published build with pnpm patch) is running in our application.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally — see below.

Local run on Node 24.8.0 (.nvmrc) with pnpm 11.9.0 (packageManager):

  • NX_BASE=<TanStack/virtual main> NX_HEAD=HEAD CI=1 pnpm run test:pr --parallel=2 --skip-nx-cache — every target passed except @tanstack/react-virtual:test:e2e, which could not launch a browser on this machine (Executable doesn't exist … chromium_headless_shell-1194).

  • After pnpm exec playwright install chromium, nx run @tanstack/react-virtual:test:e2e --skip-nx-cache passes: 36 passed.

  • nx run-many --targets=test:eslint,test:lib,test:types,test:build,build --parallel=2 --skip-nx-cache68 projects, all successful.

  • Prettier passes for the changed TypeScript files and the changeset.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset (one patch for @tanstack/react-virtual).
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Prevented development warnings when virtualized list items are measured during rendering.
    • Improved dynamic item measurement for lists with an initial scroll position.
    • Preserved synchronous updates for other measurement and scrolling interactions.

`measureElement` is passed to item elements as a ref, so React calls it during
the commit phase. A measured size that differs from `estimateSize` makes
`resizeItem` compensate scroll and raise a sync notify, and the adapter then
called `flushSync(rerender)` while React was still committing — React runs the
callback but cannot flush, and warns in development.

React sets the update priority to discrete inside `flushSync` and the commit
phase already runs at discrete priority, so for that window `flushSync` and a
plain `rerender()` produce the same lane and the same flush point. Track the
measure-from-ref window and skip `flushSync` only there; notifies from every
other path (ResizeObserver re-measures, scroll adjustments) still flush
synchronously, which `useFlushSync: false` would have given up.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview 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: 8e3f08ba-3f77-4594-a0fc-2488031f4c72

📥 Commits

Reviewing files that changed from the base of the PR and between 2c0a0ea and 1d8a1c6.

📒 Files selected for processing (3)
  • .changeset/sweet-pugs-marry.md
  • packages/react-virtual/src/index.tsx
  • packages/react-virtual/tests/index.test.tsx

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


📝 Walkthrough

Walkthrough

The React adapter now skips flushSync only while measureElement runs from a ref callback during commit. Tests cover dynamic measurement with an initial offset, and a changeset records a patch release.

Changes

React virtualizer flush handling

Layer / File(s) Summary
Commit-phase flush handling
packages/react-virtual/src/index.tsx, .changeset/sweet-pugs-marry.md
The adapter tracks ref-based measurement and skips flushSync for synchronous notifications raised during that window. Other synchronous notification paths still use flushSync.
Measurement regression coverage
packages/react-virtual/tests/index.test.tsx
The test helper accepts initialOffset, and a regression test verifies that dynamic ref-based measurement does not log a flushSync warning.

Priority: ➖ Normal

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

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: piecyk

Merge Risk: ⚪ Minimal · up to 1d8a1

The commit-phase warning is addressed without an established regression to other measurement or scrolling behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 identifies the primary change: skipping flushSync while measuring from the React ref callback.
Description check ✅ Passed The description explains the problem, fix, behavior preservation, regression test, verification results, checklist status, and changeset release impact. It satisfies the required template sections.
Linked Issues check ✅ Passed The change satisfies issue #1281. packages/react-virtual/src/index.tsx tracks the measureElement ref-callback window and calls rerender() without flushSync only for synchronous notifications i…
Out of Scope Changes check ✅ Passed The changes stay within issue #1281. The initialOffset test-helper support enables the required reproduction. The regression test verifies warning removal. The changeset records the patch release fo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.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 PR with unit tests

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.

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.

react-virtual: measuring an item from its ref callback triggers a flushSync warning

1 participant