feat: support end-anchored virtualizers#1173
Conversation
📝 WalkthroughWalkthroughThis PR adds end-anchored virtualization and chat/log controls: new Virtualizer options ( ChangesEnd-Anchored Virtualization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
examples/react/chat/vite.config.jsParsing error: "parserOptions.project" has been provided for packages/virtual-core/tests/index.test.tsParsing error: "parserOptions.project" has been provided for Comment |
🚀 Changeset Version Preview1 package(s) bumped directly, 7 bumped as dependents. 🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit bc6a315
☁️ Nx Cloud last updated this comment at |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
examples/react/chat/README.md (1)
1-6: ⚡ Quick winConsider adding a brief description of what this example demonstrates.
The README is functional but could benefit from a sentence or two explaining that this example showcases end-anchored virtualization for chat-style UIs, including prepending history, streaming messages, and auto-follow behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/react/chat/README.md` around lines 1 - 6, Update the README.md for the chat example to include a short descriptive paragraph explaining what the example demonstrates: that it showcases end-anchored virtualization for chat-style UIs, supports prepending history, streaming messages, and automatic follow/autoscroll behavior; place this sentence or two near the top under the title in examples/react/chat/README.md so developers immediately understand the key features shown by the example.packages/virtual-core/src/index.ts (1)
646-654: 💤 Low valueConsider caching key-to-index mapping for large lists.
findIndexByKeyperforms O(n) linear search. While this is only called during anchor restoration (not per frame), it could become noticeable with very large lists (10k+ items) during rapid prepend operations.For now this is acceptable since anchor restoration is infrequent, but if performance becomes an issue, consider maintaining a
Map<Key, number>index during measurement builds.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/virtual-core/src/index.ts` around lines 646 - 654, findIndexByKey currently does an O(n) scan over this.options.count using this.options.getItemKey which can be slow for very large lists; to fix, introduce a Map<Key, number> (e.g., this.keyToIndex) that is populated during measurement/build phases (the same place where item sizes/indices are computed) and used by findIndexByKey to return index in O(1); ensure the map is maintained/updated on any mutations that change indices (prepend/insert/remove/clear) or invalidated and rebuilt when measurements are recomputed so the mapping stays in sync with count and getItemKey.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@examples/react/chat/README.md`:
- Around line 1-6: Update the README.md for the chat example to include a short
descriptive paragraph explaining what the example demonstrates: that it
showcases end-anchored virtualization for chat-style UIs, supports prepending
history, streaming messages, and automatic follow/autoscroll behavior; place
this sentence or two near the top under the title in
examples/react/chat/README.md so developers immediately understand the key
features shown by the example.
In `@packages/virtual-core/src/index.ts`:
- Around line 646-654: findIndexByKey currently does an O(n) scan over
this.options.count using this.options.getItemKey which can be slow for very
large lists; to fix, introduce a Map<Key, number> (e.g., this.keyToIndex) that
is populated during measurement/build phases (the same place where item
sizes/indices are computed) and used by findIndexByKey to return index in O(1);
ensure the map is maintained/updated on any mutations that change indices
(prepend/insert/remove/clear) or invalidated and rebuilt when measurements are
recomputed so the mapping stays in sync with count and getItemKey.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0382c210-0433-4cc5-9310-0a7845749778
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
.changeset/chat-reverse-virtualization.mddocs/api/virtualizer.mdexamples/react/chat/.gitignoreexamples/react/chat/README.mdexamples/react/chat/index.htmlexamples/react/chat/package.jsonexamples/react/chat/src/index.cssexamples/react/chat/src/main.tsxexamples/react/chat/tsconfig.jsonexamples/react/chat/vite.config.jspackages/react-virtual/e2e/app/chat/index.htmlpackages/react-virtual/e2e/app/chat/main.tsxpackages/react-virtual/e2e/app/test/chat.spec.tspackages/react-virtual/e2e/app/vite.config.tspackages/react-virtual/playwright.config.tspackages/virtual-core/src/index.tspackages/virtual-core/tests/index.test.ts
76f1131 to
34bc57b
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/react-virtual/e2e/app/test/chat.spec.ts (1)
53-54: ⚡ Quick winReplace fixed sleeps with condition-based waits.
The hard-coded waits can make this spec flaky under variable CI load. Prefer
expect.poll(...)/locator-based waits tied to scroll/message state transitions instead ofwaitForTimeout(100).♻️ Suggested direction
- await page.waitForTimeout(100) + await expect + .poll(async () => { + const current = await firstVisibleMessage(page) + return current.id + }) + .toBeDefined() ... - await page.waitForTimeout(100) + await expect + .poll(async () => + page.evaluate(() => { + const container = document.querySelector('`#scroll-container`') + if (!container) throw new Error('Container not found') + return container.scrollTop + }), + ) + .toBeCloseTo(before, 0)Also applies to: 58-59, 78-79, 87-88
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react-virtual/e2e/app/test/chat.spec.ts` around lines 53 - 54, Replace the brittle page.waitForTimeout(100) calls with condition-based waits: locate the chat scroll container or message list and wait for the expected state using Playwright's expect/locator APIs (e.g., await expect(locator('selector-for-messages')).toHaveCount(expected) or await expect.poll(() => locator('selector-for-scroll').evaluate(...)).toMatch(...)); in other words, swap each page.waitForTimeout(100) (and the similar occurrences at 58-59, 78-79, 87-88) with a targeted wait that asserts the message/scroll state change (use expect.poll for custom predicates or locator.waitFor/expect.toHaveText/toHaveCount for standard conditions) so the spec waits for actual UI transitions instead of a fixed sleep.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/react-virtual/e2e/app/test/chat.spec.ts`:
- Around line 53-54: Replace the brittle page.waitForTimeout(100) calls with
condition-based waits: locate the chat scroll container or message list and wait
for the expected state using Playwright's expect/locator APIs (e.g., await
expect(locator('selector-for-messages')).toHaveCount(expected) or await
expect.poll(() => locator('selector-for-scroll').evaluate(...)).toMatch(...));
in other words, swap each page.waitForTimeout(100) (and the similar occurrences
at 58-59, 78-79, 87-88) with a targeted wait that asserts the message/scroll
state change (use expect.poll for custom predicates or
locator.waitFor/expect.toHaveText/toHaveCount for standard conditions) so the
spec waits for actual UI transitions instead of a fixed sleep.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 92aeb2de-aff1-4b1e-9e84-edc04c0b0d9d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
.changeset/chat-reverse-virtualization.mddocs/api/virtualizer.mdexamples/react/chat/.gitignoreexamples/react/chat/README.mdexamples/react/chat/index.htmlexamples/react/chat/package.jsonexamples/react/chat/src/index.cssexamples/react/chat/src/main.tsxexamples/react/chat/tsconfig.jsonexamples/react/chat/vite.config.jspackages/react-virtual/e2e/app/chat/index.htmlpackages/react-virtual/e2e/app/chat/main.tsxpackages/react-virtual/e2e/app/test/chat.spec.tspackages/react-virtual/e2e/app/vite.config.tspackages/react-virtual/playwright.config.tspackages/virtual-core/src/index.tspackages/virtual-core/tests/index.test.ts
✅ Files skipped from review due to trivial changes (3)
- examples/react/chat/package.json
- examples/react/chat/README.md
- .changeset/chat-reverse-virtualization.md
34bc57b to
bc6a315
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/react/chat/src/main.tsx`:
- Around line 123-133: The current useLayoutEffect sets didInitialScroll and
also starts a timeout for setAutoHistoryEnabled, but React will run the cleanup
immediately when didInitialScroll changes so the timeout is cleared before
firing; fix by separating responsibilities: keep the scroll logic in the
existing React.useLayoutEffect that calls virtualizer.scrollToEnd() and
setDidInitialScroll(true) (leave that effect dependent on didInitialScroll and
virtualizer), then add a separate React.useEffect with an empty dependency array
that schedules the window.setTimeout(() => setAutoHistoryEnabled(true), 250) and
returns a cleanup that clears that timeout on unmount; alternatively, delay
calling setDidInitialScroll until after the timeout completes—use setTimeout to
call setAutoHistoryEnabled(true) then setDidInitialScroll(true) if you prefer
the single-effect approach.
- Line 2: Change the default import to the named createRoot from
'react-dom/client' and update the app bootstrap to use
createRoot(...).render(...). In the component using useLayoutEffect, prevent
didInitialScroll from being set synchronously (which cancels the pending
timeout) by moving the setDidInitialScroll call into the timeout callback after
setAutoHistoryEnabled(true) (or call setAutoHistoryEnabled(true) first, then
setDidInitialScroll(true) inside the same timeout), and ensure the cleanup still
clears the timeout; refer to createRoot, useLayoutEffect, didInitialScroll,
setAutoHistoryEnabled and setDidInitialScroll to locate the changes.
In `@packages/react-virtual/e2e/app/test/chat.spec.ts`:
- Line 53: Replace the fragile fixed sleeps (calls to page.waitForTimeout(100))
in chat.spec.ts with state-based waits: after each scroll or click, use
Playwright's expect.poll or expect.toHaveJSProperty against the chat scroll
container (e.g., check its scrollTop/scrollHeight or the relevant element's
visibility) until the expected value/visibility appears; specifically locate the
occurrences of page.waitForTimeout in chat.spec.ts and swap them for
expect.poll(() =>
document.querySelector('<scroll-selector>').scrollTop).toBeGreaterThan(...) or
expect(locator('<item-selector>')).toBeVisible() so the test waits for the
actual UI state instead of a fixed timeout.
In `@packages/virtual-core/src/index.ts`:
- Around line 1527-1536: isAtEnd()/getDistanceFromEnd() use getTotalSize() -
getSize() instead of the actual DOM max scroll offset, so a DOM scroll extent
larger than virtual size can prevent end detection; update getDistanceFromEnd to
compute distance using getMaxScrollOffset() - this.getScrollOffset() (clamped to
>=0) and keep isAtEnd(threshold = this.options.scrollEndThreshold) comparing
that result to threshold; modify references in getDistanceFromEnd, isAtEnd and
any callers relying on the old computation (functions: getDistanceFromEnd,
isAtEnd, getMaxScrollOffset, getScrollOffset, getTotalSize, getSize) to ensure
followOnAppend/end-pinned behavior works with the real max scroll offset.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 671aaa81-a34f-42cb-a74f-dfe76b4598c8
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (20)
.changeset/chat-reverse-virtualization.mddocs/api/virtualizer.mddocs/chat.mddocs/config.jsondocs/introduction.mdexamples/react/chat/.gitignoreexamples/react/chat/README.mdexamples/react/chat/index.htmlexamples/react/chat/package.jsonexamples/react/chat/src/index.cssexamples/react/chat/src/main.tsxexamples/react/chat/tsconfig.jsonexamples/react/chat/vite.config.jspackages/react-virtual/e2e/app/chat/index.htmlpackages/react-virtual/e2e/app/chat/main.tsxpackages/react-virtual/e2e/app/test/chat.spec.tspackages/react-virtual/e2e/app/vite.config.tspackages/react-virtual/playwright.config.tspackages/virtual-core/src/index.tspackages/virtual-core/tests/index.test.ts
✅ Files skipped from review due to trivial changes (7)
- packages/react-virtual/e2e/app/chat/index.html
- examples/react/chat/README.md
- docs/introduction.md
- examples/react/chat/package.json
- examples/react/chat/.gitignore
- packages/react-virtual/playwright.config.ts
- .changeset/chat-reverse-virtualization.md
| @@ -0,0 +1,218 @@ | |||
| import React from 'react' | |||
| import ReactDOM from 'react-dom/client' | |||
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React 19, does react-dom/clientprovide a default export, or shouldcreateRoot be imported as a named export?
💡 Result:
React 19: import createRoot from react-dom/client as a named export (not a default export). Use: import { createRoot } from 'react-dom/client'; The official React reference for createRoot shows this named-import form [1]. The public TypeScript type definitions for react-dom/client declare createRoot as an exported function (named export) rather than a default export [2][3].
Citations:
- 1: https://react.dev/reference/react-dom/client/createRoot
- 2: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d6b527de9f5bf07aa905597563ec1dc995f84ac1/types/react-dom/client.d.ts
- 3: https://cdn.jsdelivr.net/npm/@types/react-dom@19.2.3/client.d.ts
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="examples/react/chat/src/main.tsx"
echo "== File =="
ls -la "$file"
echo
echo "== Line 1-40 =="
sed -n '1,40p' "$file" | nl -ba
echo
echo "== Around import line (1-5) =="
sed -n '1,8p' "$file" | nl -ba
echo
echo "== Around line 110-160 (lifecycle concern) =="
sed -n '105,160p' "$file" | nl -ba
echo
echo "== Around line 115-145 (extra context) =="
sed -n '115,145p' "$file" | nl -ba
echo
echo "== Search for didInitialScroll, autoHistoryEnabled, autoHistory, useLayoutEffect, timeout =="
rg -n "didInitialScroll|autoHistoryEnabled|autoHistory|useLayoutEffect|setTimeout|clearTimeout" "$file" || trueRepository: TanStack/virtual
Length of output: 232
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="examples/react/chat/src/main.tsx"
echo "== File =="
ls -la "$file"
echo
echo "== Lines 1-60 =="
sed -n '1,60p' "$file" | cat -n
echo
echo "== Lines 100-170 =="
sed -n '100,170p' "$file" | cat -n
echo
echo "== Around import (lines 1-10) =="
sed -n '1,10p' "$file" | cat -n
echo
echo "== Search for key state/effect/timeout symbols =="
rg -n "didInitialScroll|autoHistoryEnabled|autoHistory|useLayoutEffect|setTimeout|clearTimeout|scrollTo" "$file" || true
echo
echo "== Verify render/createRoot usage =="
rg -n "createRoot|ReactDOM" "$file" || trueRepository: TanStack/virtual
Length of output: 6146
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="examples/react/chat/src/main.tsx"
rg -n "setAutoHistoryEnabled|autoHistoryEnabled" "$file"Repository: TanStack/virtual
Length of output: 252
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="examples/react/chat/src/main.tsx"
rg -n "ReactDOM" "$file" || trueRepository: TanStack/virtual
Length of output: 176
Fix react-dom/client usage and ensure auto-history enablement in the React chat example
react-dom/clientis default-imported asReactDOM, butcreateRootis a named export; switch toimport { createRoot } ...and update the render call.- The
useLayoutEffectsetsdidInitialScrollimmediately, which causes the effect to rerun and clears the pendingsetTimeoutbeforesetAutoHistoryEnabled(true)can execute, leavingautoHistoryEnabledfalse.
Suggested fix
-import ReactDOM from 'react-dom/client'
+import { createRoot } from 'react-dom/client'
@@
-ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
+createRoot(document.getElementById('root')!).render(<App />)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/react/chat/src/main.tsx` at line 2, Change the default import to the
named createRoot from 'react-dom/client' and update the app bootstrap to use
createRoot(...).render(...). In the component using useLayoutEffect, prevent
didInitialScroll from being set synchronously (which cancels the pending
timeout) by moving the setDidInitialScroll call into the timeout callback after
setAutoHistoryEnabled(true) (or call setAutoHistoryEnabled(true) first, then
setDidInitialScroll(true) inside the same timeout), and ensure the cleanup still
clears the timeout; refer to createRoot, useLayoutEffect, didInitialScroll,
setAutoHistoryEnabled and setDidInitialScroll to locate the changes.
| React.useLayoutEffect(() => { | ||
| if (didInitialScroll) return | ||
| virtualizer.scrollToEnd() | ||
| setDidInitialScroll(true) | ||
|
|
||
| const id = window.setTimeout(() => { | ||
| setAutoHistoryEnabled(true) | ||
| }, 250) | ||
|
|
||
| return () => window.clearTimeout(id) | ||
| }, [didInitialScroll, virtualizer]) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React, when a useLayoutEffect dependency changes, does the previous cleanup run before the next effect callback?
💡 Result:
Yes. When the dependency array of a useLayoutEffect changes, React runs the previous effect’s cleanup function first (with the old dependency values), and then runs the next useLayoutEffect setup callback (with the new dependency values) after the commit with the updated DOM.[1] React describes the same ordering for effects generally: “After every commit with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values.”[2]
Citations:
autoHistoryEnabled timer is cleared before it can fire. (examples/react/chat/src/main.tsx:123-133)
React runs the previous effect cleanup before the next effect callback when dependencies change. Since this effect sets didInitialScroll inside the same run, the effect re-runs immediately and the cleanup clears the timeout before the 250ms elapses, so setAutoHistoryEnabled(true) likely never runs.
The suggested split into a separate useEffect still depends on didInitialScroll, so when didInitialScroll flips to true it will cancel the newly-created timeout as well.
Fix by scheduling the timeout in an effect that doesn’t re-run/cleanup on the didInitialScroll change (e.g., run once and clear on unmount), or delay the didInitialScroll update until after the timeout completes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/react/chat/src/main.tsx` around lines 123 - 133, The current
useLayoutEffect sets didInitialScroll and also starts a timeout for
setAutoHistoryEnabled, but React will run the cleanup immediately when
didInitialScroll changes so the timeout is cleared before firing; fix by
separating responsibilities: keep the scroll logic in the existing
React.useLayoutEffect that calls virtualizer.scrollToEnd() and
setDidInitialScroll(true) (leave that effect dependent on didInitialScroll and
virtualizer), then add a separate React.useEffect with an empty dependency array
that schedules the window.setTimeout(() => setAutoHistoryEnabled(true), 250) and
returns a cleanup that clears that timeout on unmount; alternatively, delay
calling setDidInitialScroll until after the timeout completes—use setTimeout to
call setAutoHistoryEnabled(true) then setDidInitialScroll(true) if you prefer
the single-effect approach.
| if (!container) throw new Error('Container not found') | ||
| container.scrollTop = 350 | ||
| }) | ||
| await page.waitForTimeout(100) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify fixed sleeps are removed from Playwright chat e2e specs
rg -nP --type=ts '\bwaitForTimeout\s*\(' packages/react-virtual/e2e/app/testRepository: TanStack/virtual
Length of output: 2348
Replace fixed page.waitForTimeout(100) sleeps in chat.spec.ts
packages/react-virtual/e2e/app/test/chat.spec.ts still contains multiple fixed sleeps (await page.waitForTimeout(100)) at lines 53, 58, 78, and 87. These can race and flake under load; replace them with state-based waits (e.g., expect.poll on scroll container scrollTop/scrollHeight or relevant visibility) after each scroll/click action.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-virtual/e2e/app/test/chat.spec.ts` at line 53, Replace the
fragile fixed sleeps (calls to page.waitForTimeout(100)) in chat.spec.ts with
state-based waits: after each scroll or click, use Playwright's expect.poll or
expect.toHaveJSProperty against the chat scroll container (e.g., check its
scrollTop/scrollHeight or the relevant element's visibility) until the expected
value/visibility appears; specifically locate the occurrences of
page.waitForTimeout in chat.spec.ts and swap them for expect.poll(() =>
document.querySelector('<scroll-selector>').scrollTop).toBeGreaterThan(...) or
expect(locator('<item-selector>')).toBeVisible() so the test waits for the
actual UI state instead of a fixed timeout.
| getDistanceFromEnd = () => { | ||
| return Math.max( | ||
| this.getTotalSize() - this.getSize() - this.getScrollOffset(), | ||
| 0, | ||
| ) | ||
| } | ||
|
|
||
| isAtEnd = (threshold = this.options.scrollEndThreshold) => { | ||
| return this.getDistanceFromEnd() <= threshold | ||
| } |
There was a problem hiding this comment.
Use the real max scroll offset for end detection.
scrollToEnd() and last-item 'end' alignment already route through getMaxScrollOffset(), but isAtEnd() still compares against getTotalSize() - getSize(). If the DOM scroll extent is larger than the virtual total (for example container padding/borders or window scrolling), a viewport can be physically pinned at the bottom and still fail isAtEnd(), which disables followOnAppend and the end-pinned resize correction path.
Suggested fix
getDistanceFromEnd = () => {
- return Math.max(
- this.getTotalSize() - this.getSize() - this.getScrollOffset(),
- 0,
- )
+ const maxScrollOffset = this.scrollElement
+ ? this.getMaxScrollOffset()
+ : Math.max(this.getTotalSize() - this.getSize(), 0)
+
+ return Math.max(maxScrollOffset - this.getScrollOffset(), 0)
}A regression where scrollHeight - clientHeight exceeds getTotalSize() - getSize() would lock this down.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| getDistanceFromEnd = () => { | |
| return Math.max( | |
| this.getTotalSize() - this.getSize() - this.getScrollOffset(), | |
| 0, | |
| ) | |
| } | |
| isAtEnd = (threshold = this.options.scrollEndThreshold) => { | |
| return this.getDistanceFromEnd() <= threshold | |
| } | |
| getDistanceFromEnd = () => { | |
| const maxScrollOffset = this.scrollElement | |
| ? this.getMaxScrollOffset() | |
| : Math.max(this.getTotalSize() - this.getSize(), 0) | |
| return Math.max(maxScrollOffset - this.getScrollOffset(), 0) | |
| } | |
| isAtEnd = (threshold = this.options.scrollEndThreshold) => { | |
| return this.getDistanceFromEnd() <= threshold | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/virtual-core/src/index.ts` around lines 1527 - 1536,
isAtEnd()/getDistanceFromEnd() use getTotalSize() - getSize() instead of the
actual DOM max scroll offset, so a DOM scroll extent larger than virtual size
can prevent end detection; update getDistanceFromEnd to compute distance using
getMaxScrollOffset() - this.getScrollOffset() (clamped to >=0) and keep
isAtEnd(threshold = this.options.scrollEndThreshold) comparing that result to
threshold; modify references in getDistanceFromEnd, isAtEnd and any callers
relying on the old computation (functions: getDistanceFromEnd, isAtEnd,
getMaxScrollOffset, getScrollOffset, getTotalSize, getSize) to ensure
followOnAppend/end-pinned behavior works with the real max scroll offset.
Summary
anchorTo: 'end'for chat-style prepend stability and bottom-pinned resize behaviorexamples/react/chatdemo for streaming, appending, and loading older messagesBundle Size
@tanstack/virtual-coreESM dist: +747 B gzip versusorigin/main@tanstack/react-virtual: 0 B deltaTesting
pnpm --filter @tanstack/virtual-core test:lib -- --runpnpm --filter @tanstack/virtual-core test:typespnpm --filter @tanstack/virtual-core test:eslintpnpm --filter @tanstack/virtual-core buildpnpm --filter @tanstack/virtual-core test:buildpnpm --filter @tanstack/react-virtual test:typespnpm --filter @tanstack/react-virtual test:eslintpnpm --filter @tanstack/react-virtual buildpnpm --filter @tanstack/react-virtual test:buildpnpm --filter tanstack-react-virtual-example-chat buildpnpm run test:docspnpm run test:sherifpnpm run test:knippnpm exec tsc -p packages/react-virtual/e2e/app/tsconfig.json --noEmitVITE_SERVER_PORT=5175 pnpm --dir packages/react-virtual exec playwright test --workers=1VITE_SERVER_PORT=5177 pnpm --dir packages/react-virtual exec playwright test app/test/chat.spec.ts --workers=1Note: default parallel React e2e showed intermittent timeouts in pre-existing smooth-scroll specs under load; the affected specs passed in isolation and the full suite passed serially.
Summary by CodeRabbit
New Features
Documentation
Examples
Tests