Documentación/Guías
Listas virtuales
Ordena miles de filas con TanStack Virtual o con useVirtualList de VueUse — dale el borrador al virtualizador, usa el id como clave de las filas y mantén montada la fila arrastrada.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
La ordenación se basa en los datos: el borrador items es un array nuevo en cada reordenación,
las filas tienen como clave su ítem y las animaciones solo necesitan las filas montadas. Así que
cualquier virtualizador que renderice a partir de un array funciona — dale el borrador en lugar de
tu array.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import TaskList from './TaskList.vue';
</script>
<template>
<DnDProvider>
<TaskList />
</DnDProvider>
</template>Las reglas
- Dale al virtualizador el borrador
items, no tu array de origen. - Usa la clave del ítem como clave de las filas, nunca el índice. Los ejemplos oficiales de TanStack y VueUse usan el índice; cámbialo.
- Pasa los ids a
makeSortableItemcomo getter,{ id: () => props.item.id }: el virtualizador puede darle otro ítem a un componente de fila. - Guarda los arrays grandes en un
shallowRef. La biblioteca nunca recorre tus datos en profundidad. - Mantén el placeholder del mismo tamaño que la fila: oculta su contenido, no lo colapses.
- Usa un overscan de al menos 5 filas (VueUse: al menos 1), para que las filas estén montadas antes de que llegue el desplazamiento automático.
- No animes
height,marginnipaddingde las filas, y no pongaswill-changeen el contenedor desplazable.
VueUse useVirtualList
La demo de arriba. Todas las filas tienen la misma altura, y el cálculo de VueUse sigue siendo 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>
itemHeightes la altura real de la fila, bordes incluidos. No pongas márgenes entre filas: VueUse no los cuenta.- La lista ordenable es el elemento exterior, porque
containerPropsvincula su propiarefal contenedor desplazable. El desplazamiento automático encuentra igualmente ese contenedor: desplaza los contenedores que hay bajo el puntero. - VueUse no puede mantener una fila montada: la fila arrastrada se desmonta cuando la desplazas fuera de la vista. No pasa nada, consulta Qué sigue montado.
TanStack Virtual
TanStack mide cada fila, así que las filas pueden ser tan altas como su contenido. También puede mantener montada la fila arrastrada por mucho que te desplaces:
Cargando la 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>
Qué hace cada pieza:
| Pieza | Por qué |
|---|---|
count y getItemKey a partir de items | El virtualizador renderiza el borrador, así que las filas se mueven mientras arrastras. |
Un solo computed alrededor de las opciones | getItemKey y rangeExtractor deben ser funciones nuevas tras cada reordenación, o TanStack conserva las medidas y el rango anteriores. |
measureElement con data-index | Cada fila se mide al renderizarse: estimateSize es solo una primera estimación. |
rangeExtractor con activeIndex | Añade la fila arrastrada al rango renderizado cuando sale de él al desplazarse. activeIndex es -1 cuando no se arrastra ninguna fila de esta lista. |
onChange: () => captureLayout() | Una nueva medición mueve filas después de que la biblioteca haya animado una reordenación. captureLayout() anima también ese movimiento. Fuera de un arrastre no hace nada. |
¿Cuándo sale la fila arrastrada del rango? Su hueco sigue al puntero mientras el puntero está
sobre la lista. Lleva el puntero justo debajo de la lista y mantenlo ahí: el desplazamiento
automático continúa, el hueco se queda donde estaba y pronto queda muy por encima de las filas en
pantalla. La fijación lo mantiene montado, para que el drop aterrice en un elemento real. Una
lista con placeholder: 'indicator' mantiene la fila arrastrada en su sitio durante todo el
arrastre, así que ahí importa todavía más.
- Fijar requiere filas posicionadas una a una, en absoluto, como arriba. Un virtualizador que renderiza un único bloque con padding no puede añadir una fila lejana.
- El componente de fila pasa su id como getter:
makeSortableItem(el, { id: () => props.note.id }). - Mantén el
anchorTo: 'start'de TanStack, el valor por defecto. Conlanes, usa@tanstack/virtual-core3.19 o posterior (@tanstack/vue-virtuallo instala).
Columnas kanban virtuales
Cada columna es su propia lista ordenable y su propio virtualizador, y las listas comparten un
group. Tres columnas de 1000 tarjetas renderizan unas pocas decenas:
Cargando la 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>Una tarjeta que llega de otra columna todavía no tiene medida en esta columna. Estímala con el
tamaño real de la tarjeta arrastrada, activeSize, leído en el mismo 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
};
}),
);
- Da a cada columna sus datos como un
shallowRef: la lista escribe en él sus drops, y 1000 tarjetas no reciben proxies profundos. activeSizees el tamaño del elemento arrastrado al inicio del arrastre. Suma lo que tu hueco pone alrededor de la tarjeta: aquí el espacio de debajo (GAP) es el padding del hueco, así que el virtualizador también lo mide.- La cabecera cuenta
items.length, el borrador: cambia en el momento en que una tarjeta entra o sale.
Qué sigue montado
La fila arrastrada puede desmontarse mientras arrastras: salió del rango al desplazarse, o el virtualizador recicló su componente. El arrastre sobrevive. El overlay sigue mostrando el ítem, la selección se conserva por id y el ítem aterriza donde lo pone el borrador. Las animaciones solo tocan las filas montadas.
Cuando el virtualizador lo permita, mantén de todos modos montada la fila arrastrada, como hace la
receta de TanStack con rangeExtractor. La lista te dice qué fila es:
| Devuelto por la lista | Para qué sirve |
|---|---|
items | El borrador: dáselo al virtualizador. |
activeIndex | El índice de la fila que hay que mantener montada en el borrador de esta lista, -1 cuando no está aquí. |
activeKey | La clave de esa fila, null cuando no está aquí. |
activeSize | El tamaño de la fila arrastrada al inicio del arrastre: estima con él una fila entrante. |
captureLayout() | Llámalo justo antes de un cambio de maquetación que no hizo la biblioteca (una nueva medición). Anima las filas desde donde estaban. Fuera de un arrastre no hace nada. |
Árboles
makeSortableTree devuelve un items plano con las filas visibles, así que un árbol se
virtualiza como una lista: con node.key como clave, y con activeIndex y activeKey indicando
la fila que hay que mantener montada. Consulta Árboles.