Skip to content

docs(vue-virtual): use onUpdated to prevent scroll jumping#1125

Merged
piecyk merged 4 commits intoTanStack:mainfrom
Mini-ghost:docs/vue-dynamic-measure-timing
Mar 16, 2026
Merged

docs(vue-virtual): use onUpdated to prevent scroll jumping#1125
piecyk merged 4 commits intoTanStack:mainfrom
Mini-ghost:docs/vue-dynamic-measure-timing

Conversation

@Mini-ghost
Copy link
Contributor

@Mini-ghost Mini-ghost commented Feb 17, 2026

🎯 Changes

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

Summary by CodeRabbit

  • Refactor
    • Updated Vue dynamic virtualizer examples to improve item measurement handling. The examples now use a collection-based approach for tracking virtual item elements and batch measurement operations, with lifecycle hooks ensuring measurements occur after mount and updates for more reliable rendering of dynamically-sized items.

@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2026

⚠️ No Changeset found

Latest commit: 9717dcb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Mini-ghost Mini-ghost force-pushed the docs/vue-dynamic-measure-timing branch from ecdfd90 to 893ad75 Compare February 17, 2026 05:05
@piecyk piecyk requested a review from Copilot March 12, 2026 18:32
@nx-cloud
Copy link

nx-cloud bot commented Mar 12, 2026

View your CI Pipeline Execution ↗ for commit 9717dcb

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 13s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 16s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-16 07:26:37 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 12, 2026

More templates

@tanstack/angular-virtual

npm i https://pkg.pr.new/@tanstack/angular-virtual@1125

@tanstack/lit-virtual

npm i https://pkg.pr.new/@tanstack/lit-virtual@1125

@tanstack/react-virtual

npm i https://pkg.pr.new/@tanstack/react-virtual@1125

@tanstack/solid-virtual

npm i https://pkg.pr.new/@tanstack/solid-virtual@1125

@tanstack/svelte-virtual

npm i https://pkg.pr.new/@tanstack/svelte-virtual@1125

@tanstack/virtual-core

npm i https://pkg.pr.new/@tanstack/virtual-core@1125

@tanstack/vue-virtual

npm i https://pkg.pr.new/@tanstack/vue-virtual@1125

commit: 9717dcb

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the Vue “dynamic” virtualizer examples to re-measure items after Vue DOM updates (using onUpdated) to address scroll stutter/jump issues reported in the Vue dynamic height demos.

Changes:

  • Replaced per-item ref callback measurement with collecting item elements via a template ref array.
  • Added measureAll() and invoked it from onMounted and onUpdated to measure rendered items post-render.
  • Adjusted Vue imports accordingly across the affected example components.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
examples/vue/dynamic/src/components/RowVirtualizerDynamicWindow.vue Switches to batched measuring via template refs + onUpdated for window-scrolling dynamic rows.
examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue Switches to batched measuring via template refs + onUpdated for container-scrolling dynamic rows.
examples/vue/dynamic/src/components/GridVirtualizerDynamic.vue Switches to batched measuring via template refs + onUpdated for dynamic grid row measurement.
examples/vue/dynamic/src/components/ColumnVirtualizerDynamic.vue Switches to batched measuring via template refs + onUpdated for dynamic columns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to 77
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measureAll never calls rowVirtualizer.value.measureElement(null). In @tanstack/virtual-core, passing null is what triggers cleanup of disconnected nodes (unobserving/removing stale entries from elementsCache). Without that, items that scroll out/unmount can remain observed/retained. Consider calling measureElement(null) once per update (or using a callback ref that forwards null on unmount) in addition to measuring the current elements.

Copilot uses AI. Check for mistakes.
if (!el) {
return
}
const virtualItemEls = shallowRef([])
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shallowRef([]) with an empty array literal infers never[] in TypeScript, which can make el in forEach be never and cause type errors when passing it to measureElement. Consider explicitly typing virtualItemEls (e.g. as an array of HTMLElement | null).

Suggested change
const virtualItemEls = shallowRef([])
const virtualItemEls = shallowRef<(HTMLElement | null)[]>([])

Copilot uses AI. Check for mistakes.
Comment on lines +69 to 73
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measureAll never calls rowVirtualizer.value.measureElement(null). In @tanstack/virtual-core, passing null is what triggers cleanup of disconnected nodes (unobserving/removing stale entries from elementsCache). Without that, items that scroll out/unmount can remain observed/retained. Consider calling measureElement(null) once per update (or using a callback ref that forwards null on unmount) in addition to measuring the current elements.

Copilot uses AI. Check for mistakes.
}

rowVirtualizer.value.measureElement(el)
const virtualItemEls = shallowRef([])
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shallowRef([]) with an empty array literal infers never[] in TypeScript, which can make el in forEach be never and cause type errors when passing it to measureElement. Consider explicitly typing virtualItemEls (e.g. as an array of HTMLElement | null).

Suggested change
const virtualItemEls = shallowRef([])
const virtualItemEls = shallowRef<(HTMLElement | null)[]>([])

Copilot uses AI. Check for mistakes.
Comment on lines +108 to 112
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measureAll never calls rowVirtualizer.value.measureElement(null). In @tanstack/virtual-core, passing null is what triggers cleanup of disconnected nodes (unobserving/removing stale entries from elementsCache). Without that, rows that scroll out/unmount can remain observed/retained. Consider calling measureElement(null) once per update (or using a callback ref that forwards null on unmount) in addition to measuring the current elements.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +80
onMounted(measureAll)
onUpdated(measureAll)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onUpdated(measureAll) runs after every component update, including scroll-driven updates as virtualRows changes. Measuring all rendered items on every update can be expensive (forces layout reads) and may reintroduce scroll jank. Consider a more targeted trigger (e.g. defer measurement from an element ref callback post-render, or only measure newly mounted items) to avoid repeated full measurement passes.

Copilot uses AI. Check for mistakes.
}

rowVirtualizer.value.measureElement(el)
const virtualItemEls = shallowRef([])
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shallowRef([]) with an empty array literal infers never[] in TypeScript, which can make el in forEach be never and cause type errors when passing it to measureElement. Consider explicitly typing virtualItemEls (e.g. as an array of HTMLElement | null).

Suggested change
const virtualItemEls = shallowRef([])
const virtualItemEls = shallowRef<(HTMLElement | null)[]>([])

Copilot uses AI. Check for mistakes.
Comment on lines +59 to 63
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) columnVirtualizer.value.measureElement(el)
})
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measureAll never calls columnVirtualizer.value.measureElement(null). In @tanstack/virtual-core, passing null is what triggers cleanup of disconnected nodes (unobserving/removing stale entries from elementsCache). Without that, columns that scroll out/unmount can remain observed/retained. Consider calling measureElement(null) once per update (or using a callback ref that forwards null on unmount) in addition to measuring the current elements.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +76
onMounted(measureAll)
onUpdated(measureAll)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onUpdated(measureAll) runs after every component update, including scroll-driven updates as virtualRows changes. Measuring all rendered items on every update can be expensive (forces layout reads) and may reintroduce scroll jank. Consider a more targeted trigger (e.g. defer measurement from an element ref callback post-render, or only measure newly mounted items) to avoid repeated full measurement passes.

Copilot uses AI. Check for mistakes.
if (!el) {
return
}
const virtualItemEls = shallowRef([])
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shallowRef([]) with an empty array literal infers never[] in TypeScript, which can make el in forEach be never and cause type errors when passing it to measureElement. Consider explicitly typing virtualItemEls (e.g. as an array of HTMLElement | null).

Suggested change
const virtualItemEls = shallowRef([])
const virtualItemEls = shallowRef<(HTMLElement | null)[]>([])

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@piecyk piecyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add those rowVirtualizer.value.measureElement(null) to all measureAll

@Mini-ghost Mini-ghost requested a review from piecyk March 15, 2026 03:01
@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Refactors Vue virtualizer components to replace per-element ref callbacks with a fixed array ref approach. Introduces a measureAll() function that iterates collected element references and triggers measurements on component mount and update phases to fix dynamic height scrolling issues.

Changes

Cohort / File(s) Summary
Vue Dynamic Virtualizer Components
examples/vue/dynamic/src/components/ColumnVirtualizerDynamic.vue, examples/vue/dynamic/src/components/GridVirtualizerDynamic.vue, examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue, examples/vue/dynamic/src/components/RowVirtualizerDynamicWindow.vue
Changes ref binding from :ref="measureElement" to ref="virtualItemEls" to collect multiple DOM elements. Replaces inline measurement callbacks with a measureAll() function that iterates collected refs and measures each element. Adds onMounted and onUpdated lifecycle hooks to trigger re-measurement after mount and updates. Introduces shallowRef array for element tracking.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Scroll jumping? Not anymore, I say!
Four virtualizers fixed the buggy way,
With refs collected and measured with care,
Up and down now flows so fair!
The dynamic heights no longer dismay! 📜✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: switching measurement timing to use onUpdated lifecycle hook in Vue examples to prevent scroll jumping issues.
Description check ✅ Passed The description includes required sections with all critical information: closes three related issues (#619, #925, #1028), contributing guide confirmed, release impact marked as docs-only.
Linked Issues check ✅ Passed Changes directly address the root cause across all three linked issues by adjusting measurement timing via onUpdated and adding cache cleanup to prevent scroll jumping/stuttering when scrolling upward in Vue dynamic examples.
Out of Scope Changes check ✅ Passed All changes are scoped to Vue example components (ColumnVirtualizerDynamic, GridVirtualizerDynamic, RowVirtualizerDynamic, RowVirtualizerDynamicWindow), directly addressing the scroll jumping issues identified in linked issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use TruffleHog to scan for secrets in your code with verification capabilities.

Add a TruffleHog config file (e.g. trufflehog-config.yml, trufflehog.yml) to your project to customize detectors and scanning behavior. The tool runs only when a config file is present.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue (1)

71-81: Add a browser regression for scrolling back up.

This exact interaction has regressed across multiple issues, and the PR notes local tests weren't confirmed. A small end-to-end check that scrolls near the end and then back up slowly would make this fix much safer to keep.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue` around lines
71 - 81, Add a browser end-to-end regression that reproduces scrolling near the
end and then scrolling back up slowly for the RowVirtualizerDynamic.vue
virtualizer: write a test (Playwright/Cypress) that mounts the
RowVirtualizerDynamic component, waits for onMounted/onUpdated to call
measureAll (which uses virtualItemEls and rowVirtualizer.measureElement),
programmatically scrolls the virtual container to near the bottom, waits for
measurements/frames, then scrolls back up slowly in small increments and asserts
no jump/regression (items render smoothly and scroll position updates as
expected). Ensure the test targets the component named RowVirtualizerDynamic.vue
and exercises the measureAll flow (virtualItemEls + rowVirtualizer) so future
changes trigger this regression check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue`:
- Around line 71-81: Add a browser end-to-end regression that reproduces
scrolling near the end and then scrolling back up slowly for the
RowVirtualizerDynamic.vue virtualizer: write a test (Playwright/Cypress) that
mounts the RowVirtualizerDynamic component, waits for onMounted/onUpdated to
call measureAll (which uses virtualItemEls and rowVirtualizer.measureElement),
programmatically scrolls the virtual container to near the bottom, waits for
measurements/frames, then scrolls back up slowly in small increments and asserts
no jump/regression (items render smoothly and scroll position updates as
expected). Ensure the test targets the component named RowVirtualizerDynamic.vue
and exercises the measureAll flow (virtualItemEls + rowVirtualizer) so future
changes trigger this regression check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0766215e-15f6-4b4a-8f85-af9284c222ca

📥 Commits

Reviewing files that changed from the base of the PR and between c2f1c39 and 9717dcb.

📒 Files selected for processing (4)
  • examples/vue/dynamic/src/components/ColumnVirtualizerDynamic.vue
  • examples/vue/dynamic/src/components/GridVirtualizerDynamic.vue
  • examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue
  • examples/vue/dynamic/src/components/RowVirtualizerDynamicWindow.vue

@piecyk piecyk merged commit 6360f79 into TanStack:main Mar 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants