Documentação/Guias
Listas virtuais
Ordene milhares de linhas com TanStack Virtual ou com o useVirtualList do VueUse — passe o rascunho ao virtualizador, identifique as linhas por id e mantenha a linha arrastada montada.
Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês
A ordenação é guiada pelos dados: o rascunho items é um array novo a cada reordenação, as linhas
são identificadas pelo item, e as animações só precisam das linhas que estão montadas. Então
qualquer virtualizador que renderiza a partir de um array funciona — passe a ele o rascunho em vez
do seu array.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import TaskList from './TaskList.vue';
</script>
<template>
<DnDProvider>
<TaskList />
</DnDProvider>
</template>As regras
- Dê ao virtualizador o rascunho
items, não o seu array de origem. - Use a key do item como key das linhas, nunca o índice. Os exemplos oficiais do TanStack e do VueUse usam o índice; mude isso.
- Passe os ids para
makeSortableItemcomo getter,{ id: () => props.item.id }: o virtualizador pode entregar outro item a um componente de linha. - Guarde arrays grandes em um
shallowRef. A biblioteca nunca percorre os seus dados em profundidade. - Mantenha o placeholder do mesmo tamanho da linha: esconda o conteúdo dele, não o colapse.
- Use overscan de pelo menos 5 linhas (VueUse: pelo menos 1), para que as linhas estejam montadas antes de a rolagem automática chegar a elas.
- Não anime
height,marginoupaddingdas linhas, e não coloquewill-changeno elemento que rola.
VueUse useVirtualList
A demo acima. Todas as linhas têm a mesma altura, e as contas do VueUse continuam 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>
itemHeighté a altura real da linha, bordas incluídas. Não coloque margens entre as linhas: o VueUse não as conta.- A lista ordenável é o elemento externo, porque
containerPropsliga o seu própriorefao elemento que rola. A rolagem automática ainda encontra esse elemento: ela rola os contêineres sob o ponteiro. - O VueUse não consegue manter uma linha montada: a linha arrastada é desmontada quando você a rola para fora da vista. Tudo bem, veja O que fica montado.
TanStack Virtual
O TanStack mede cada linha, então as linhas podem ser tão altas quanto o conteúdo delas. Ele também consegue manter a linha arrastada montada, não importa o quanto você role para longe dela:
Carregando a demo…
<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>
O que cada peça faz:
| Peça | Por quê |
|---|---|
count e getItemKey a partir de items | O virtualizador renderiza o rascunho, então as linhas se movem enquanto você arrasta. |
Um único computed em volta das opções | getItemKey e rangeExtractor precisam ser funções novas depois de cada reordenação, senão o TanStack mantém as medidas e o intervalo antigos. |
measureElement com data-index | Cada linha é medida ao ser renderizada: estimateSize é só um primeiro palpite. |
rangeExtractor com activeIndex | Adiciona a linha arrastada ao intervalo renderizado quando ela sai dele pela rolagem. activeIndex é -1 quando nenhuma linha desta lista é arrastada. |
onChange: () => captureLayout() | Uma nova medição move as linhas depois que a biblioteca animou uma reordenação. captureLayout() anima esse movimento também. Fora de um arraste, ele não faz nada. |
Quando a linha arrastada sai do intervalo? A posição dela segue o ponteiro enquanto o ponteiro
está sobre a lista. Leve o ponteiro para logo abaixo da lista e segure ali: a rolagem automática
continua, a posição fica onde estava, e logo ela está bem acima das linhas na tela. A fixação a
mantém montada, então o drop pousa em um elemento real. Uma lista com placeholder: 'indicator'
mantém a linha arrastada no lugar durante todo o arraste, então ali isso importa ainda mais.
- A fixação precisa de linhas posicionadas uma a uma, de forma absoluta, como acima. Um virtualizador que renderiza um único bloco com padding não consegue adicionar uma linha distante.
- O componente de linha passa o id dele como getter:
makeSortableItem(el, { id: () => props.note.id }). - Mantenha o
anchorTo: 'start'do TanStack, o padrão. Comlanes, use@tanstack/virtual-core3.19 ou mais recente (o@tanstack/vue-virtualo instala).
Colunas kanban virtuais
Cada coluna é a sua própria lista ordenável e o seu próprio virtualizador, e as listas
compartilham um group. Três colunas de 1.000 cards renderizam algumas dezenas:
Carregando a demo…
<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>Um card que chega de outra coluna ainda não tem medida nesta coluna. Estime-o com o tamanho real
do card arrastado, activeSize, lido no mesmo 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
};
}),
);
- Dê a cada coluna os dados dela como um
shallowRef: a lista grava os drops nele, e 1.000 cards não ganham proxies profundos. activeSizeé o tamanho do elemento arrastado no início do arraste. Some o que a sua posição coloca em volta do card: aqui o espaço embaixo dele (GAP) é o padding da posição, então o virtualizador também o mede.- O cabeçalho conta
items.length, o rascunho: ele muda no instante em que um card entra ou sai.
O que fica montado
A linha arrastada pode ser desmontada enquanto você arrasta: ela saiu do intervalo pela rolagem, ou o virtualizador reciclou o componente dela. O arraste sobrevive a isso. O overlay continua mostrando o item, a seleção é mantida por id, e o item pousa onde o rascunho o coloca. As animações só mexem nas linhas que estão montadas.
Onde o virtualizador permite, mantenha a linha arrastada montada mesmo assim, como a receita do
TanStack faz com rangeExtractor. A lista diz qual linha é essa:
| Retornado pela lista | Para que serve |
|---|---|
items | O rascunho: passe-o ao virtualizador. |
activeIndex | O índice da linha a manter montada no rascunho desta lista, -1 quando ela não está aqui. |
activeKey | A key dessa linha, null quando ela não está aqui. |
activeSize | O tamanho da linha arrastada no início do arraste: estime com ele uma linha que chega. |
captureLayout() | Chame-o logo antes de uma mudança de layout que a biblioteca não fez (uma nova medição). Ele anima as linhas a partir de onde estavam. Fora de um arraste, não faz nada. |
Árvores
makeSortableTree retorna um items plano com as linhas visíveis, então uma árvore é virtualizada
como uma lista: com node.key como key, e activeIndex e activeKey indicando a linha a manter
montada. Veja Árvores.