-
-
Notifications
You must be signed in to change notification settings - Fork 452
feat(react-virtual): add useVirtualizerSnapshot for React Compiler compatibility #1241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kklem0
wants to merge
3
commits into
TanStack:main
Choose a base branch
from
kklem0:feat/use-virtualizer-snapshot
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/react-virtual': minor | ||
| --- | ||
|
|
||
| Add `useVirtualizerSnapshot` and `useWindowVirtualizerSnapshot`: variants of the existing hooks that return `virtualItems` / `totalSize` as immutable snapshot data via `useSyncExternalStore`, with the stable `virtualizer` instance alongside for imperative APIs. Components consuming the snapshot hooks are compatible with React Compiler — the compiler skips components calling `useVirtualizer` as a known-incompatible API (#736, #743, #1119), while snapshot consumers compile and memoize correctly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>useVirtualizerSnapshot + React Compiler</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="./main.tsx"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import React from 'react' | ||
| import ReactDOM from 'react-dom/client' | ||
| import { useVirtualizerSnapshot } from '@tanstack/react-virtual' | ||
|
|
||
| const ITEM_SIZE = 40 | ||
| const COUNT = 1000 | ||
|
|
||
| /** | ||
| * Regression page for https://github.com/TanStack/virtual/issues/736 using | ||
| * `useVirtualizerSnapshot` under React Compiler. | ||
| * | ||
| * Unlike `useVirtualizer` — which the compiler skips as a known-incompatible | ||
| * API — components calling the snapshot hook ARE compiled. The snapshot's | ||
| * identity changes whenever the computed items change, so the compiler's | ||
| * memoized output stays fresh: item-500 must appear after scrolling. This | ||
| * page must stay free of patterns that make the compiler bail (no ref | ||
| * mutation during render, etc.), or it silently stops guarding the compiled | ||
| * path. | ||
| */ | ||
| const App = () => { | ||
| const parentRef = React.useRef<HTMLDivElement>(null) | ||
|
|
||
| const { virtualItems, totalSize, virtualizer } = useVirtualizerSnapshot({ | ||
| count: COUNT, | ||
| getScrollElement: () => parentRef.current, | ||
| estimateSize: () => ITEM_SIZE, | ||
| overscan: 2, | ||
| }) | ||
|
|
||
| // Commit counter for the spec, kept outside React-managed content so the | ||
| // component stays compiler-clean (no ref/global mutation during render). | ||
| const commitCountRef = React.useRef(0) | ||
| React.useEffect(() => { | ||
| commitCountRef.current += 1 | ||
| const el = document.getElementById('commit-count') | ||
| if (el) el.textContent = String(commitCountRef.current) | ||
| }) | ||
|
|
||
| return ( | ||
| <div> | ||
| <div id="commit-count" data-testid="commit-count" /> | ||
| <button id="scroll-to-500" onClick={() => virtualizer.scrollToIndex(500)}> | ||
| Scroll to 500 | ||
| </button> | ||
|
|
||
| <div | ||
| ref={parentRef} | ||
| id="scroll-container" | ||
| style={{ height: 400, overflow: 'auto' }} | ||
| > | ||
| <div | ||
| id="inner" | ||
| style={{ | ||
| position: 'relative', | ||
| width: '100%', | ||
| height: totalSize, | ||
| }} | ||
| > | ||
| {virtualItems.map((v) => ( | ||
| <div | ||
| key={v.key} | ||
| data-testid={`item-${v.index}`} | ||
| ref={virtualizer.measureElement} | ||
| data-index={v.index} | ||
| style={{ | ||
| position: 'absolute', | ||
| top: 0, | ||
| left: 0, | ||
| width: '100%', | ||
| height: ITEM_SIZE, | ||
| transform: `translateY(${v.start}px)`, | ||
| }} | ||
| > | ||
| Row {v.index} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| ReactDOM.createRoot(document.getElementById('root')!).render(<App />) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { expect, test } from '@playwright/test' | ||
|
|
||
| const ITEM_SIZE = 40 | ||
| const COUNT = 1000 | ||
|
|
||
| test.describe('useVirtualizerSnapshot under React Compiler', () => { | ||
| test('renders items on initial load', async ({ page }) => { | ||
| await page.goto('/snapshot-hook/') | ||
|
|
||
| await expect(page.locator('[data-testid="item-0"]')).toBeVisible() | ||
| await expect(page.locator('[data-testid="item-0"]')).toContainText('Row 0') | ||
|
|
||
| // The sizer height comes from the snapshot's totalSize. | ||
| const inner = page.locator('#inner') | ||
| await expect(inner).toHaveAttribute( | ||
| 'style', | ||
| new RegExp(`height:\\s*${COUNT * ITEM_SIZE}px`), | ||
| ) | ||
| }) | ||
|
|
||
| test('items update after scrolling — the #736 regression, compiled', async ({ | ||
| page, | ||
| }) => { | ||
| await page.goto('/snapshot-hook/') | ||
|
|
||
| await expect(page.locator('[data-testid="item-0"]')).toBeVisible() | ||
|
|
||
| await page.click('#scroll-to-500') | ||
|
|
||
| // With `useVirtualizer` a compiled consumer would keep serving the | ||
| // initial items forever. The snapshot hook publishes a new identity, so | ||
| // the compiled component re-derives its output. | ||
| await expect(page.locator('[data-testid="item-500"]')).toBeVisible({ | ||
| timeout: 5000, | ||
| }) | ||
| const style = | ||
| (await page.locator('[data-testid="item-500"]').getAttribute('style')) ?? | ||
| '' | ||
| expect(style).toMatch(/translateY\(20000px\)/) | ||
|
|
||
| // And the initial row is no longer rendered. | ||
| await expect(page.locator('[data-testid="item-0"]')).toHaveCount(0) | ||
| }) | ||
|
|
||
| test('re-renders are driven by snapshot changes', async ({ page }) => { | ||
| await page.goto('/snapshot-hook/') | ||
| await expect(page.locator('[data-testid="item-0"]')).toBeVisible() | ||
|
|
||
| const before = Number( | ||
| await page.locator('[data-testid="commit-count"]').textContent(), | ||
| ) | ||
| expect(before).toBeGreaterThan(0) | ||
|
|
||
| await page.click('#scroll-to-500') | ||
| await expect(page.locator('[data-testid="item-500"]')).toBeVisible() | ||
|
|
||
| const after = Number( | ||
| await page.locator('[data-testid="commit-count"]').textContent(), | ||
| ) | ||
| expect(after).toBeGreaterThan(before) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.