---
title: "仮想リスト"
description: "TanStack Virtual や VueUse の useVirtualList で数千行を並べ替えます——仮想化ライブラリにはドラフトを渡し、行を id でキー付けし、ドラッグ中の行をマウントしたままにします。"
canonical_url: "https://zizigy.github.io/vue-dnd-kit/ja/docs/guides/virtual-lists"
---

> Index of every page on this site: [llms.txt](https://zizigy.github.io/vue-dnd-kit/llms.txt)

並べ替えはデータ駆動です。ドラフトの `items` は並べ替えのたびに新しい配列になり、行はアイテムでキー付けされ、アニメーションにはマウントされている行だけがあれば十分です。そのため、配列から描画する仮想化ライブラリなら何でも使えます——配列の代わりにドラフトを渡してください。

<example height="32.5rem" hint="10,000 行のうち DOM にあるのはわずか。1 行をドラッグするか、ドラッグしながらスクロールしてみましょう" name="virtual-list">



</example>

## ルール

1. 仮想化ライブラリには、元の配列ではなく**ドラフトの items** を渡します。
2. **行はアイテムのキーでキー付けし**、インデックスは使いません。TanStack と VueUse の公式例はインデックスでキー付けしているので、変更してください。
3. `makeSortableItem` には id を**ゲッター**として渡します：`{ id: () => props.item.id }`。仮想化ライブラリは行コンポーネントに別のアイテムを渡すことがあります。
4. 大きな配列は **shallowRef** に保持します。ライブラリがデータを深く走査することはありません。
5. プレースホルダーは行と同じサイズに保ちます。中身は隠しても、潰さないでください。
6. オーバースキャンは少なくとも 5 行（VueUse では少なくとも 1 行）にして、自動スクロールが届く前に行がマウントされるようにします。
7. 行の `height`、`margin`、`padding` をアニメーションせず、スクロールコンテナーに `will-change` を付けないでください。

## VueUse useVirtualList

上のデモです。すべての行が同じ高さで、VueUse の計算は O(1) のままです。

```vue [TaskList.vue]
<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` はボーダーを含む行の実際の高さです。行の間にマージンを入れないでください。VueUse はそれを数えません。
- `containerProps` が自身の `ref` をスクロールコンテナーにバインドするので、並べ替え可能なリストは外側の要素にします。自動スクロールはそれでもスクロールコンテナーを見つけます。ポインターの下のコンテナーをスクロールするからです。
- VueUse は行をマウントしたままにできません。ドラッグ中の行は、スクロールで見えなくなるとアンマウントされます。それで問題ありません。[マウントされたままのもの](#what-stays-mounted)を参照してください。

## TanStack Virtual

TanStack はすべての行を測定するので、行の高さを中身に合わせられます。また、どれだけ遠くへスクロールしても、ドラッグ中の行をマウントしたままにできます。

<example height="34.5rem" hint="メモをリストのすぐ下までドラッグして保ってみましょう。ドラッグ中の行は固定されたままです" name="virtual-tanstack">



</example>

```vue [NoteList.vue]
<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>
```

それぞれの部分の役割：

<table>
<thead>
  <tr>
    <th>
      部分
    </th>
    
    <th>
      理由
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        items
      </code>
      
       からの <code>
        count
      </code>
      
       と <code>
        getItemKey
      </code>
    </td>
    
    <td>
      仮想化ライブラリがドラフトを描画するので、ドラッグ中に行が動きます。
    </td>
  </tr>
  
  <tr>
    <td>
      オプション全体を 1 つの <code>
        computed
      </code>
      
       で囲む
    </td>
    
    <td>
      並べ替えのたびに <code>
        getItemKey
      </code>
      
       と <code>
        rangeExtractor
      </code>
      
       が新しい関数である必要があります。そうでないと TanStack は古い測定結果と範囲を保ちます。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-index
      </code>
      
       付きの <code>
        measureElement
      </code>
    </td>
    
    <td>
      各行は描画時に測定されます。<code>
        estimateSize
      </code>
      
       は最初の推定にすぎません。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        activeIndex
      </code>
      
       付きの <code>
        rangeExtractor
      </code>
    </td>
    
    <td>
      ドラッグ中の行が描画範囲から外れたとき、それを範囲に加えます。このリストの行がドラッグされていなければ <code>
        activeIndex
      </code>
      
       は <code>
        -1
      </code>
      
       です。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onChange: () => captureLayout()
      </code>
    </td>
    
    <td>
      再測定は、ライブラリが並べ替えをアニメーションした後に行を動かします。<code>
        captureLayout()
      </code>
      
       はその移動もアニメーションします。ドラッグ中でなければ何もしません。
    </td>
  </tr>
</tbody>
</table>

ドラッグ中の行が範囲から外れるのはどんなときでしょうか？ ポインターがリストの上にある間、その枠はポインターに追従します。ポインターをリストのすぐ下へ動かしてそこに保つと、自動スクロールは続きますが、枠は元の場所にとどまり、すぐに画面上の行よりはるか上になります。固定によってマウントされたままなので、ドロップは実際の要素に着地します。`placeholder: 'indicator'` のリストはドラッグ中ずっとドラッグ中の行を元の場所に置いたままにするので、そこではなおさら重要です。

- 固定するには、上のように行を 1 つずつ絶対配置する必要があります。パディング付きの 1 つのブロックとして描画する仮想化ライブラリでは、遠く離れた行を追加できません。
- 行コンポーネントは id をゲッターとして渡します：`makeSortableItem(el, { id: () => props.note.id })`。
- TanStack の `anchorTo: 'start'`（デフォルト）はそのままにしてください。`lanes` を使う場合は、`@tanstack/virtual-core` 3.19 以降（`@tanstack/vue-virtual` がインストールします）を使ってください。

## 仮想化したカンバンの列

各列はそれぞれ独自の並べ替え可能なリストと独自の仮想化ライブラリを持ち、リストは `group` を共有します。1,000 枚のカードの列が 3 つあっても、描画されるのは数十枚です。

<example height="34rem" hint="1,000 枚のカードの列の間でカードを動かしてみましょう" name="virtual-kanban">



</example>

別の列から入ってくるカードには、この列での測定結果がまだありません。同じ computed で読み取った、ドラッグ中のカードの実際のサイズ `activeSize` で推定してください。

```ts [Column.vue]
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
    };
  }),
);
```

- 各列のデータは `shallowRef` として渡してください。リストはそこへドロップを書き込み、1,000 枚のカードに深いプロキシが作られることはありません。
- `activeSize` はドラッグ開始時のドラッグされた要素のサイズです。枠がカードの周りに加えるものを足してください。ここではカードの下の隙間（`GAP`）が枠のパディングなので、仮想化ライブラリはそれも測定します。
- ヘッダーはドラフトである `items.length` を数えるので、カードが入ったり出たりした瞬間に変わります。

## マウントされたままのもの

ドラッグ中の行は、ドラッグ中にアンマウントされることがあります：スクロールで範囲から外れた、または仮想化ライブラリがコンポーネントを再利用した場合です。それでもドラッグは続きます。オーバーレイはアイテムを表示し続け、選択は id で保たれ、アイテムはドラフトが置く場所に着地します。アニメーションが扱うのはマウントされている行だけです。

それでも、仮想化ライブラリが対応していれば、TanStack のレシピが `rangeExtractor` で行っているように、ドラッグ中の行はマウントしたままにしてください。どの行かはリストが教えてくれます。

<table>
<thead>
  <tr>
    <th>
      リストが返すもの
    </th>
    
    <th>
      用途
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        items
      </code>
    </td>
    
    <td>
      ドラフト：仮想化ライブラリに渡します。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        activeIndex
      </code>
    </td>
    
    <td>
      このリストのドラフトで、マウントしたままにする行のインデックス。ここにない場合は <code>
        -1
      </code>
      
      。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        activeKey
      </code>
    </td>
    
    <td>
      その行のキー。ここにない場合は <code>
        null
      </code>
      
      。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        activeSize
      </code>
    </td>
    
    <td>
      ドラッグ開始時のドラッグ中の行のサイズ：入ってくる行の推定に使います。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        captureLayout()
      </code>
    </td>
    
    <td>
      ライブラリによらないレイアウトの変化（再測定）の直前に呼びます。行を元の位置からアニメーションします。ドラッグ中でなければ何もしません。
    </td>
  </tr>
</tbody>
</table>

## ツリー

`makeSortableTree` は表示されている行をフラットな `items` として返すので、ツリーもリストと同じように仮想化できます：`node.key` でキー付けし、`activeIndex` と `activeKey` がマウントしたままにする行を示します。[ツリー](https://zizigy.github.io/docs/guides/trees)を参照してください。

## Sitemap

See the full [sitemap](https://zizigy.github.io/vue-dnd-kit/sitemap.md) for all pages.
