docs(vue-virtual): use onUpdated to prevent scroll jumping#1125
docs(vue-virtual): use onUpdated to prevent scroll jumping#1125Mini-ghost wants to merge 2 commits intoTanStack:mainfrom
onUpdated to prevent scroll jumping#1125Conversation
|
ecdfd90 to
893ad75
Compare
|
View your CI Pipeline Execution ↗ for commit 893ad75
☁️ Nx Cloud last updated this comment at |
There was a problem hiding this comment.
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 fromonMountedandonUpdatedto 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.
| function measureAll() { | ||
| virtualItemEls.value.forEach((el) => { | ||
| if (el) rowVirtualizer.value.measureElement(el) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| if (!el) { | ||
| return | ||
| } | ||
| const virtualItemEls = shallowRef([]) |
There was a problem hiding this comment.
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).
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
| function measureAll() { | ||
| virtualItemEls.value.forEach((el) => { | ||
| if (el) rowVirtualizer.value.measureElement(el) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| rowVirtualizer.value.measureElement(el) | ||
| const virtualItemEls = shallowRef([]) |
There was a problem hiding this comment.
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).
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
| function measureAll() { | ||
| virtualItemEls.value.forEach((el) => { | ||
| if (el) rowVirtualizer.value.measureElement(el) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| onMounted(measureAll) | ||
| onUpdated(measureAll) |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| rowVirtualizer.value.measureElement(el) | ||
| const virtualItemEls = shallowRef([]) |
There was a problem hiding this comment.
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).
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
| function measureAll() { | ||
| virtualItemEls.value.forEach((el) => { | ||
| if (el) columnVirtualizer.value.measureElement(el) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| onMounted(measureAll) | ||
| onUpdated(measureAll) |
There was a problem hiding this comment.
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.
| if (!el) { | ||
| return | ||
| } | ||
| const virtualItemEls = shallowRef([]) |
There was a problem hiding this comment.
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).
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
piecyk
left a comment
There was a problem hiding this comment.
Let's add those rowVirtualizer.value.measureElement(null) to all measureAll
| function measureAll() { | ||
| virtualItemEls.value.forEach((el) => { | ||
| if (el) columnVirtualizer.value.measureElement(el) | ||
| }) |
There was a problem hiding this comment.
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.
| 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) | |
| }) |
🎯 Changes
✅ Checklist
pnpm run test:pr.🚀 Release Impact