Documentation/Guides
Listes virtuelles
Triez des milliers de lignes avec TanStack Virtual ou le useVirtualList de VueUse — donnez le brouillon au virtualiseur, utilisez l'id des lignes comme clé, et gardez la ligne glissée montée.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Le tri est piloté par les données : le brouillon items est un nouveau tableau à chaque
réordonnancement, les lignes ont pour clé leur item, et les animations n'ont besoin que des
lignes montées. N'importe quel virtualiseur qui rend à partir d'un tableau fonctionne donc —
donnez-lui le brouillon au lieu de votre tableau.
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import TaskList from './TaskList.vue';
</script>
<template>
<DnDProvider>
<TaskList />
</DnDProvider>
</template>Les règles
- Donnez au virtualiseur le brouillon
items, pas votre tableau source. - Utilisez la clé de l'item comme clé des lignes, jamais l'index. Les exemples officiels de TanStack et de VueUse utilisent l'index ; changez cela.
- Passez les ids à
makeSortableItemsous forme de getter,{ id: () => props.item.id }: le virtualiseur peut confier un autre item à un composant de ligne. - Gardez les gros tableaux dans un
shallowRef. La bibliothèque ne parcourt jamais vos données en profondeur. - Gardez au placeholder la même taille que la ligne : masquez son contenu, ne le réduisez pas.
- Prévoyez un overscan d'au moins 5 lignes (VueUse : au moins 1), pour que les lignes soient montées avant que le défilement automatique ne les atteigne.
- N'animez pas la
height, lamarginou lepaddingdes lignes, et ne mettez pas dewill-changesur le conteneur défilant.
VueUse useVirtualList
La démo ci-dessus. Toutes les lignes ont la même hauteur, et le calcul de VueUse reste en O(1) :
<script setup lang="ts">
import { shallowRef, useTemplateRef } from 'vue';
import { useVirtualList } from '@vueuse/core';
import { makeSortableList } from '@vue-dnd-kit/core';
const ROW = 44;
const tasks = shallowRef(makeTasks(10_000));
// The outer element is the sortable list: `containerProps` owns the scroller's ref.
const listRef = useTemplateRef<HTMLElement>('list');
const { items } = makeSortableList(listRef, { items: tasks });
// Fed with the draft `items`, so the rows move while you drag.
const { list, containerProps, wrapperProps } = useVirtualList(items, { itemHeight: ROW, overscan: 8 });
</script>
<template>
<div ref="list">
<div v-bind="containerProps" style="height: 308px">
<div v-bind="wrapperProps">
<!-- Keyed by id, not by index as in VueUse's docs. -->
<Row v-for="{ data } in list" :key="data.id" :task="data" :height="ROW" />
</div>
</div>
</div>
</template>
itemHeightest la hauteur réelle de la ligne, bordures comprises. Ne mettez pas de marges entre les lignes : VueUse ne les compte pas.- La liste triable est l'élément extérieur, parce que
containerPropslie sa proprerefau conteneur défilant. Le défilement automatique trouve quand même ce conteneur : il fait défiler les conteneurs sous le pointeur. - VueUse ne peut pas garder une ligne montée : la ligne glissée est démontée quand vous la faites défiler hors de vue. Ce n'est pas un problème, voir Ce qui reste monté.
TanStack Virtual
TanStack mesure chaque ligne : les lignes peuvent donc être aussi hautes que leur contenu. Il peut aussi garder la ligne glissée montée, aussi loin que vous fassiez défiler :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import NoteList from './NoteList.vue';
</script>
<template>
<DnDProvider>
<NoteList />
</DnDProvider>
</template><script setup lang="ts">
import { computed, shallowRef, useTemplateRef } from 'vue';
import { defaultRangeExtractor, useVirtualizer, type Range } from '@tanstack/vue-virtual';
import { makeSortableList } from '@vue-dnd-kit/core';
const notes = shallowRef(makeNotes(5_000));
// The scroller is the sortable list: the library sorts it and auto-scrolls it.
const scrollRef = useTemplateRef<HTMLElement>('scroll');
const { items, activeIndex, captureLayout } = makeSortableList(scrollRef, { items: notes });
// One computed that reads the draft once: TanStack memoizes the measurements and the range
// on `getItemKey` and `rangeExtractor`, so they must be new functions after every reorder.
const virtualizer = useVirtualizer(
computed(() => {
const arr = items.value;
const pin = activeIndex.value;
return {
count: arr.length,
getScrollElement: () => scrollRef.value,
estimateSize: () => 92,
overscan: 6,
getItemKey: (index: number) => arr[index]!.id,
// The dragged row stays mounted, however far you scroll from it.
rangeExtractor: (range: Range) => {
const indexes = defaultRangeExtractor(range);
return pin < 0 || indexes.includes(pin) ? indexes : [...indexes, pin].sort((a, b) => a - b);
},
// Measurements land after the library's animation pass: animate them too.
onChange: () => captureLayout(),
};
}),
);
const rows = computed(() => virtualizer.value.getVirtualItems());
const measure = (el: unknown) => virtualizer.value.measureElement(el as HTMLElement | null);
</script>
<template>
<div ref="scroll" style="height: 340px; overflow: auto; contain: strict">
<div :style="{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }">
<div
v-for="row in rows"
:key="(row.key as string)"
:ref="measure"
:data-index="row.index"
:style="{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${row.start}px)` }"
>
<NoteRow :note="items[row.index]!" />
</div>
</div>
</div>
</template>
Ce que fait chaque morceau :
| Morceau | Pourquoi |
|---|---|
count et getItemKey à partir de items | Le virtualiseur rend le brouillon : les lignes bougent donc pendant le glissement. |
Un seul computed autour des options | getItemKey et rangeExtractor doivent être de nouvelles fonctions après chaque réordonnancement, sinon TanStack garde les anciennes mesures et l'ancienne plage. |
measureElement avec data-index | Chaque ligne est mesurée au moment où elle est rendue : estimateSize n'est qu'une première estimation. |
rangeExtractor avec activeIndex | Ajoute la ligne glissée à la plage rendue quand elle en sort en défilant. activeIndex vaut -1 quand aucune ligne de cette liste n'est glissée. |
onChange: () => captureLayout() | Une nouvelle mesure déplace des lignes après que la bibliothèque a animé un réordonnancement. captureLayout() anime aussi ce déplacement. En dehors d'un glissement, il ne fait rien. |
Quand la ligne glissée quitte-t-elle la plage ? Son emplacement suit le pointeur tant que le
pointeur est au-dessus de la liste. Placez le pointeur juste sous la liste et maintenez-le là : le
défilement automatique continue, l'emplacement reste là où il était, et il se retrouve vite loin
au-dessus des lignes à l'écran. L'épinglage le garde monté, pour que le dépôt atterrisse sur un
vrai élément. Une liste avec placeholder: 'indicator' garde la ligne glissée en place pendant
tout le glissement : c'est donc encore plus important dans ce cas.
- L'épinglage demande des lignes positionnées une à une, en absolu, comme ci-dessus. Un virtualiseur qui rend un seul bloc avec du padding ne peut pas ajouter une ligne éloignée.
- Le composant de ligne passe son id sous forme de getter :
makeSortableItem(el, { id: () => props.note.id }). - Gardez l'
anchorTo: 'start'de TanStack, la valeur par défaut. Aveclanes, utilisez@tanstack/virtual-core3.19 ou plus récent (@tanstack/vue-virtuall'installe).
Colonnes kanban virtuelles
Chaque colonne est sa propre liste triable et son propre virtualiseur, et les listes partagent un
group. Trois colonnes de 1 000 cartes en rendent quelques dizaines :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
import { makeColumns } from './cards';
// 3,000 cards; each column renders the dozen you can see.
const columns = makeColumns(1_000);
</script>
<template>
<DnDProvider>
<div class="board">
<Column v-for="column in columns" :key="column.id" :column="column" />
</div>
</DnDProvider>
</template>
<style scoped>
/* Three columns; on a phone they keep their width and the board scrolls sideways. */
.board {
display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
gap: 10px;
width: 100%;
max-width: 680px;
margin: 0 auto;
overflow-x: auto;
}
</style>Une carte qui arrive d'une autre colonne n'a pas encore de mesure dans cette colonne. Estimez-la
à la taille réelle de la carte glissée, activeSize, lue dans le même computed :
const { items, activeIndex, activeSize, captureLayout } = makeSortableList(scrollRef, {
id: props.column.id,
items: props.column.cards, // a shallowRef per column
group: 'board',
});
const virtualizer = useVirtualizer(
computed(() => {
const arr = items.value;
const pin = activeIndex.value;
const size = activeSize.value;
return {
count: arr.length,
getScrollElement: () => scrollRef.value,
// A card coming from another column gets its real height before it is measured.
estimateSize: (index: number) => (index === pin && size ? size.height + GAP : 72),
// …getItemKey, rangeExtractor and onChange as above
};
}),
);
- Donnez à chaque colonne ses données sous forme de
shallowRef: la liste y écrit ses dépôts, et 1 000 cartes ne reçoivent aucun proxy profond. activeSizeest la taille de l'élément glissé au début du glissement. Ajoutez ce que votre emplacement met autour de la carte : ici, l'espace en dessous (GAP) est le padding de l'emplacement, que le virtualiseur mesure donc aussi.- L'en-tête compte
items.length, le brouillon : il change dès qu'une carte entre ou sort.
Ce qui reste monté
La ligne glissée peut être démontée pendant le glissement : elle est sortie de la plage en défilant, ou le virtualiseur a recyclé son composant. Le glissement y survit. L'overlay continue d'afficher l'item, la sélection est gardée par id, et l'item atterrit là où le brouillon le place. Les animations ne touchent que les lignes montées.
Quand le virtualiseur le permet, gardez quand même la ligne glissée montée, comme le fait la
recette TanStack avec rangeExtractor. La liste vous dit de quelle ligne il s'agit :
| Renvoyé par la liste | À quoi il sert |
|---|---|
items | Le brouillon : donnez-le au virtualiseur. |
activeIndex | L'index de la ligne à garder montée dans le brouillon de cette liste, -1 quand elle n'y est pas. |
activeKey | La clé de cette ligne, null quand elle n'y est pas. |
activeSize | La taille de la ligne glissée au début du glissement : estimez-en une ligne entrante. |
captureLayout() | Appelez-la juste avant un changement de mise en page que la bibliothèque n'a pas fait (une nouvelle mesure). Elle anime les lignes depuis l'endroit où elles étaient. En dehors d'un glissement, elle ne fait rien. |
Arbres
makeSortableTree renvoie un items à plat des lignes visibles : un arbre se virtualise donc
comme une liste, avec node.key comme clé, et activeIndex et activeKey qui désignent la
ligne à garder montée. Voir les arbres.