Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions examples/vue/dynamic/src/components/ColumnVirtualizerDynamic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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'

Expand All @@ -54,13 +54,14 @@ const virtualColumns = computed(() => columnVirtualizer.value.getVirtualItems())

const totalSize = computed(() => columnVirtualizer.value.getTotalSize())

const measureElement = (el) => {
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.

columnVirtualizer.value.measureElement(el)

return undefined
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) columnVirtualizer.value.measureElement(el)
})
Comment on lines +59 to +62
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)
})

}
Comment on lines +59 to 63
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.

onMounted(measureAll)
onUpdated(measureAll)
</script>
23 changes: 12 additions & 11 deletions examples/vue/dynamic/src/components/GridVirtualizerDynamic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -103,13 +103,14 @@ const width = computed(() => {
: [0, 0]
})

const measureElement = (el) => {
if (!el) {
return
}

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.

return undefined
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})
}
Comment on lines +108 to 112
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.

onMounted(measureAll)
onUpdated(measureAll)
</script>
19 changes: 10 additions & 9 deletions examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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'

Expand All @@ -68,13 +68,14 @@ const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())

const totalSize = computed(() => rowVirtualizer.value.getTotalSize())

const measureElement = (el) => {
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.

rowVirtualizer.value.measureElement(el)

return undefined
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})
}
Comment on lines +73 to 77
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.

onMounted(measureAll)
onUpdated(measureAll)
Comment on lines +79 to +80
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.
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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'

Expand Down Expand Up @@ -64,13 +64,14 @@ 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([])
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.

return undefined
function measureAll() {
virtualItemEls.value.forEach((el) => {
if (el) rowVirtualizer.value.measureElement(el)
})
}
Comment on lines +69 to 73
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.

onMounted(measureAll)
onUpdated(measureAll)
Comment on lines +75 to +76
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.
</script>
Loading