Skip to content

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

Open
Mini-ghost wants to merge 2 commits intoTanStack:mainfrom
Mini-ghost:docs/vue-dynamic-measure-timing
Open

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

Conversation

@Mini-ghost
Copy link

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

@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2026

⚠️ No Changeset found

Latest commit: 893ad75

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 893ad75

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

☁️ Nx Cloud last updated this comment at 2026-03-12 18:34:01 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: fccf707

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

Comment on lines +59 to +62
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) columnVirtualizer.value.measureElement(el)
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

Calling measureElement(null) first ensures stale elements from previous renders are cleaned up before re-registering the current ones. Without it, if virtual items get recycled (scrolling removes old DOM nodes), those old nodes may linger in the cache until the ResizeObserver's own callback eventually notices they're disconnected. Adding the null call makes cleanup immediate and deterministic.

Suggested change
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) columnVirtualizer.value.measureElement(el)
})
function measureAll() {
rowVirtualizer.value.measureElement(null)
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})

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