diff --git a/.changeset/angry-poets-visit.md b/.changeset/angry-poets-visit.md new file mode 100644 index 000000000..4958cbfa6 --- /dev/null +++ b/.changeset/angry-poets-visit.md @@ -0,0 +1,10 @@ +--- +'@tanstack/react-virtual': patch +--- + +feat(react-virtual): add `useFlushSync` option + +Adds a React-specific `useFlushSync` option to control whether `flushSync` is used for synchronous scroll correction during measurement. + +The default behavior remains unchanged (`useFlushSync: true`) to preserve the best scrolling experience. +Disabling it avoids the React 19 warning about calling `flushSync` during render, at the cost of potentially increased visible whitespace during fast scrolling with dynamically sized items. diff --git a/docs/framework/react/react-virtual.md b/docs/framework/react/react-virtual.md index e32a982ee..338d32052 100644 --- a/docs/framework/react/react-virtual.md +++ b/docs/framework/react/react-virtual.md @@ -9,7 +9,7 @@ The `@tanstack/react-virtual` adapter is a wrapper around the core virtual logic ```tsx function useVirtualizer( options: PartialKeys< - VirtualizerOptions, + ReactVirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer @@ -22,7 +22,7 @@ This function returns a standard `Virtualizer` instance configured to work with ```tsx function useWindowVirtualizer( options: PartialKeys< - VirtualizerOptions, + ReactVirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' @@ -32,3 +32,44 @@ function useWindowVirtualizer( ``` This function returns a window-based `Virtualizer` instance configured to work with the window as the scrollElement. + +## React-Specific Options + +### `useFlushSync` + +```tsx +type ReactVirtualizerOptions = + VirtualizerOptions & { + useFlushSync?: boolean + } +``` + +Both `useVirtualizer` and `useWindowVirtualizer` accept a `useFlushSync` option that controls whether React's `flushSync` is used for synchronous updates. + +- **Type**: `boolean` +- **Default**: `true` +- **Description**: When `true`, the virtualizer will use `flushSync` from `react-dom` to ensure synchronous rendering during scroll events. This provides the most accurate scrolling behavior but may impact performance in some scenarios. + +#### When to disable `useFlushSync` + +You may want to set `useFlushSync: false` in the following scenarios: + +- **React 19 compatibility**: In React 19, you may see the following console warning when scrolling: + ``` + 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. + ``` + Setting `useFlushSync: false` will eliminate this warning by allowing React to batch updates naturally. +- **Performance optimization**: If you experience performance issues with rapid scrolling on lower-end devices +- **Testing environments**: When running tests that don't require synchronous DOM updates +- **Non-critical lists**: When slight visual delays during scrolling are acceptable for better overall performance + +#### Example + +```tsx +const virtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => parentRef.current, + estimateSize: () => 50, + useFlushSync: false, // Disable synchronous updates +}) +``` diff --git a/packages/react-virtual/package.json b/packages/react-virtual/package.json index c25600380..a5def4a5b 100644 --- a/packages/react-virtual/package.json +++ b/packages/react-virtual/package.json @@ -30,7 +30,7 @@ "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", "build": "vite build", - "test:e2e": "playwright test" + "test:e2e": "../../node_modules/.bin/playwright test" }, "type": "module", "types": "dist/esm/index.d.ts", diff --git a/packages/react-virtual/src/index.tsx b/packages/react-virtual/src/index.tsx index d835e7c4b..313c3d4f9 100644 --- a/packages/react-virtual/src/index.tsx +++ b/packages/react-virtual/src/index.tsx @@ -16,18 +16,29 @@ export * from '@tanstack/virtual-core' const useIsomorphicLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect +export type ReactVirtualizerOptions< + TScrollElement extends Element | Window, + TItemElement extends Element, +> = VirtualizerOptions & { + useFlushSync?: boolean +} + function useVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, ->( - options: VirtualizerOptions, -): Virtualizer { +>({ + useFlushSync = true, + ...options +}: ReactVirtualizerOptions): Virtualizer< + TScrollElement, + TItemElement +> { const rerender = React.useReducer(() => ({}), {})[1] const resolvedOptions: VirtualizerOptions = { ...options, onChange: (instance, sync) => { - if (sync) { + if (useFlushSync && sync) { flushSync(rerender) } else { rerender() @@ -58,7 +69,7 @@ export function useVirtualizer< TItemElement extends Element, >( options: PartialKeys< - VirtualizerOptions, + ReactVirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer { @@ -72,7 +83,7 @@ export function useVirtualizer< export function useWindowVirtualizer( options: PartialKeys< - VirtualizerOptions, + ReactVirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset'