diff --git a/.changeset/fix-react-19-flushsync.md b/.changeset/fix-react-19-flushsync.md new file mode 100644 index 00000000..143e3b7c --- /dev/null +++ b/.changeset/fix-react-19-flushsync.md @@ -0,0 +1,11 @@ +--- +"@tanstack/react-virtual": patch +--- + +fix: defer flushSync to microtask for React 19 compatibility + +React 19 throws a warning when `flushSync` is called from inside a lifecycle method (`useLayoutEffect`). This change wraps the `flushSync` call in `queueMicrotask()` to defer it to a microtask, allowing React to complete its current render cycle before forcing the synchronous update. + +Fixes #1094 + +This fixes the warning: "flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering." diff --git a/packages/react-virtual/src/index.tsx b/packages/react-virtual/src/index.tsx index d835e7c4..a7da4a0e 100644 --- a/packages/react-virtual/src/index.tsx +++ b/packages/react-virtual/src/index.tsx @@ -28,7 +28,7 @@ function useVirtualizerBase< ...options, onChange: (instance, sync) => { if (sync) { - flushSync(rerender) + queueMicrotask(() => flushSync(rerender)) } else { rerender() } diff --git a/packages/react-virtual/tests/index.test.tsx b/packages/react-virtual/tests/index.test.tsx index c7348dc2..05c69c77 100644 --- a/packages/react-virtual/tests/index.test.tsx +++ b/packages/react-virtual/tests/index.test.tsx @@ -138,7 +138,7 @@ test('should render given dynamic size', async () => { expect(renderer).toHaveBeenCalledTimes(3) }) -test('should render given dynamic size after scroll', () => { +test('should render given dynamic size after scroll', async () => { render() expect(screen.queryByText('Row 0')).toBeInTheDocument() @@ -153,6 +153,9 @@ test('should render given dynamic size after scroll', () => { target: { scrollTop: 400 }, }) + // Wait for microtask to complete (flushSync is deferred via queueMicrotask) + await new Promise((resolve) => queueMicrotask(() => resolve())) + expect(screen.queryByText('Row 2')).not.toBeInTheDocument() expect(screen.queryByText('Row 3')).toBeInTheDocument() expect(screen.queryByText('Row 6')).toBeInTheDocument()