Skip to content

Conversation

@channyeintun
Copy link

@channyeintun channyeintun commented Dec 27, 2025

React 19 throws a warning when flushSync is called from inside a lifecycle method (useLayoutEffect). This change adds a new deferFlushSync option that users can enable to defer flushSync to a microtask, suppressing the warning.

Fixes #1094

🎯 Changes

Problem

When using @tanstack/react-virtual with React 19, users see the following warning in the console:

flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.

This occurs because flushSync(rerender) is called inside the onChange callback, which can be triggered during useLayoutEffect via instance._willUpdate().

Solution

Added a new deferFlushSync option that users can enable to defer the flushSync call:

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 50,
  deferFlushSync: true, // Opt-in to suppress React 19 warning
})

When enabled, the flushSync call is wrapped in queueMicrotask():

  if (sync) {
-   flushSync(rerender)
+   if (options.deferFlushSync) {
+     queueMicrotask(() => flushSync(rerender))
+   } else {
+     flushSync(rerender)
+   }
  }

Why opt-in instead of default?

  • The synchronous flushSync behavior is intentional for scroll correction during measurement
  • Deferring to a microtask may cause visible white space gaps during fast scrolling with dynamic measurements
  • Users experiencing the React 19 warning can choose to accept this tradeoff

Files Changed

  • packages/virtual-core/src/index.ts - Add deferFlushSync option to VirtualizerOptions
  • packages/react-virtual/src/index.tsx - Conditionally wrap flushSync based on option
  • packages/react-virtual/tests/index.test.tsx - Add test for deferFlushSync behavior

✅ 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).

…bility

React 19 throws a warning when flushSync is called from inside a lifecycle method (useLayoutEffect). This change defers the flushSync call to a microtask using queueMicrotask(), allowing React to complete its current render cycle before forcing the synchronous update.

Fixes the warning: 'flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering.'
@changeset-bot
Copy link

changeset-bot bot commented Dec 27, 2025

🦋 Changeset detected

Latest commit: fa4acc3

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

This PR includes changesets to release 7 packages
Name Type
@tanstack/react-virtual Patch
@tanstack/virtual-core Patch
@tanstack/angular-virtual Patch
@tanstack/lit-virtual Patch
@tanstack/solid-virtual Patch
@tanstack/svelte-virtual Patch
@tanstack/vue-virtual 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

@channyeintun
Copy link
Author

@piecyk Hi, could you review this PR please?

@piecyk
Copy link
Collaborator

piecyk commented Dec 28, 2025

@channyeintun Thanks for taking a look at this.

Unfortunately, deferring flushSync via queueMicrotask() fundamentally changes the behavior here.

In this code path, flushSync is intentionally used to synchronously apply the scroll correction during measurement to limit visible white space while scrolling. By pushing it into a microtask, React has already completed the render/commit phase and the scroll has progressed further, effectively making this flushSync a no-op for its original purpose.

In practice this reintroduces the exact issue flushSync was added to solve: noticeable gaps during fast scrolling with dynamic measurements.

Instead of changing the default behavior, I think we should make this configurable under a flag in @tanstack/react-virtual.

ps: the warning itself doesn’t really affect anything in this case.

Instead of unconditionally deferring flushSync via queueMicrotask,
add a configurable option that allows users to opt-in to this behavior.

- Add deferFlushSync option to VirtualizerOptions (default: false)
- Update react-virtual to conditionally defer flushSync based on option
- Update tests to cover both synchronous (default) and deferred behaviors

This addresses reviewer feedback that unconditionally deferring flushSync
changes the scroll correction behavior and reintroduces visible gaps
during fast scrolling with dynamic measurements.

Fixes TanStack#1094
@channyeintun channyeintun changed the title fix(react-virtual): defer flushSync to microtask for React 19 compatibility feat: add deferFlushSync option for React 19 compatibility Dec 29, 2025
@channyeintun
Copy link
Author

@channyeintun Thanks for taking a look at this.

Unfortunately, deferring flushSync via queueMicrotask() fundamentally changes the behavior here.

In this code path, flushSync is intentionally used to synchronously apply the scroll correction during measurement to limit visible white space while scrolling. By pushing it into a microtask, React has already completed the render/commit phase and the scroll has progressed further, effectively making this flushSync a no-op for its original purpose.

In practice this reintroduces the exact issue flushSync was added to solve: noticeable gaps during fast scrolling with dynamic measurements.

Instead of changing the default behavior, I think we should make this configurable under a flag in @tanstack/react-virtual.

ps: the warning itself doesn’t really affect anything in this case.

Good call - I didn't consider that deferring breaks the scroll correction. I've changed it to an opt-in flag (deferFlushSync) so the default stays synchronous. Users who hit the warning can enable it if they're okay with the tradeoff.

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.

flushSync was called from inside a lifecycle method

2 participants