-
-
Notifications
You must be signed in to change notification settings - Fork 427
docs(vue-virtual): use onUpdated to prevent scroll jumping
#1125
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
Changes from all commits
8ffbf22
893ad75
ac0f5f1
9717dcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ | |
| v-for="virtualColumn in virtualColumns" | ||
| :key="virtualColumn.key" | ||
| :data-index="virtualColumn.index" | ||
| :ref="measureElement" | ||
| ref="virtualItemEls" | ||
| :class="virtualColumn.index % 2 ? 'ListItemOdd' : 'ListItemEven'" | ||
| :style="{ | ||
| position: 'absolute', | ||
|
|
@@ -35,7 +35,7 @@ | |
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { ref, computed, type VNodeRef } from 'vue' | ||
| import { computed, onMounted, onUpdated, ref, shallowRef } from 'vue' | ||
| import { useVirtualizer } from '@tanstack/vue-virtual' | ||
| import { generateSentences } from './utils' | ||
|
|
||
|
|
@@ -54,13 +54,15 @@ const virtualColumns = computed(() => columnVirtualizer.value.getVirtualItems()) | |
|
|
||
| const totalSize = computed(() => columnVirtualizer.value.getTotalSize()) | ||
|
|
||
| const measureElement = (el) => { | ||
| if (!el) { | ||
| return | ||
| } | ||
| const virtualItemEls = shallowRef([]) | ||
|
|
||
| columnVirtualizer.value.measureElement(el) | ||
|
|
||
| return undefined | ||
| function measureAll() { | ||
| columnVirtualizer.value.measureElement(null) | ||
| virtualItemEls.value.forEach((el) => { | ||
| if (el) columnVirtualizer.value.measureElement(el) | ||
| }) | ||
piecyk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
+59
to
64
|
||
|
|
||
| onMounted(measureAll) | ||
| onUpdated(measureAll) | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,7 @@ | |||||
| <template v-for="virtualRow in virtualRows" :key="virtualRow.key"> | ||||||
| <div | ||||||
| :data-index="virtualRow.index" | ||||||
| :ref="measureElement" | ||||||
| ref="virtualItemEls" | ||||||
| :style="{ | ||||||
| position: 'absolute', | ||||||
| top: 0, | ||||||
|
|
@@ -47,9 +47,9 @@ | |||||
| </template> | ||||||
|
|
||||||
| <script setup lang="ts"> | ||||||
| import { ref, computed, onMounted, type VNodeRef } from 'vue' | ||||||
| import { useWindowVirtualizer, useVirtualizer } from '@tanstack/vue-virtual' | ||||||
| import { generateData, generateColumns } from './utils' | ||||||
| import { computed, onMounted, onUpdated, ref, shallowRef } from 'vue' | ||||||
| import { useVirtualizer, useWindowVirtualizer } from '@tanstack/vue-virtual' | ||||||
| import { generateColumns, generateData } from './utils' | ||||||
|
|
||||||
| const columns = generateColumns(30) | ||||||
| const data = generateData(columns) | ||||||
|
|
@@ -103,13 +103,15 @@ const width = computed(() => { | |||||
| : [0, 0] | ||||||
| }) | ||||||
|
|
||||||
| const measureElement = (el) => { | ||||||
| if (!el) { | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| rowVirtualizer.value.measureElement(el) | ||||||
| const virtualItemEls = shallowRef([]) | ||||||
|
||||||
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -35,7 +35,7 @@ | |||||
| v-for="virtualRow in virtualRows" | ||||||
| :key="virtualRow.key" | ||||||
| :data-index="virtualRow.index" | ||||||
| :ref="measureElement" | ||||||
| ref="virtualItemEls" | ||||||
| :class="virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'" | ||||||
| > | ||||||
| <div style="padding: 10px 0"> | ||||||
|
|
@@ -50,7 +50,7 @@ | |||||
| </template> | ||||||
|
|
||||||
| <script setup lang="ts"> | ||||||
| import { ref, computed } from 'vue' | ||||||
| import { computed, onMounted, onUpdated, ref, shallowRef } from 'vue' | ||||||
| import { useVirtualizer } from '@tanstack/vue-virtual' | ||||||
| import { generateSentences } from './utils' | ||||||
|
|
||||||
|
|
@@ -68,13 +68,15 @@ const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems()) | |||||
|
|
||||||
| const totalSize = computed(() => rowVirtualizer.value.getTotalSize()) | ||||||
|
|
||||||
| const measureElement = (el) => { | ||||||
| if (!el) { | ||||||
| return | ||||||
| } | ||||||
| const virtualItemEls = shallowRef([]) | ||||||
|
||||||
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
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
AI
Mar 12, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -22,7 +22,7 @@ | |||||
| v-for="virtualRow in virtualRows" | ||||||
| :key="virtualRow.key" | ||||||
| :data-index="virtualRow.index" | ||||||
| :ref="measureElement" | ||||||
| ref="virtualItemEls" | ||||||
| :class="virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'" | ||||||
| > | ||||||
| <div style="padding: 10px 0"> | ||||||
|
|
@@ -36,7 +36,7 @@ | |||||
| </template> | ||||||
|
|
||||||
| <script setup lang="ts"> | ||||||
| import { ref, computed, onMounted } from 'vue' | ||||||
| import { computed, onMounted, onUpdated, ref, shallowRef } from 'vue' | ||||||
| import { useWindowVirtualizer } from '@tanstack/vue-virtual' | ||||||
| import { generateSentences } from './utils' | ||||||
|
|
||||||
|
|
@@ -64,13 +64,15 @@ const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems()) | |||||
|
|
||||||
| const totalSize = computed(() => rowVirtualizer.value.getTotalSize()) | ||||||
|
|
||||||
| const measureElement = (el) => { | ||||||
| if (!el) { | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| rowVirtualizer.value.measureElement(el) | ||||||
| const virtualItemEls = shallowRef([]) | ||||||
|
||||||
| const virtualItemEls = shallowRef([]) | |
| const virtualItemEls = shallowRef<(HTMLElement | null)[]>([]) |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
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
AI
Mar 12, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 infersnever[]in TypeScript, which can makeelinforEachbeneverand cause type errors when passing it tomeasureElement. Consider explicitly typingvirtualItemEls(e.g. as an array ofHTMLElement | null).