[Next.js]はじめてのdnd-kit　その2（単一コンテナ並べ替え）

## はじめに
Next.jsを触ってみたいと、学習用に簡単なアプリを作っており、その中で新たに試したことを記事にしています。
同じようにこれからNext.js始めてみようという方の参考になれば嬉しいです。

## やりたいこと
アイテムをドラッグアンドドロップで並び替えを行いたい。
今回は、単一コンテナ内でのアイテムの並び替えとなります。

前回dnd-kitの基本編を記事にしました。
続きとして「dnd-kit/sortable」を使っていきます。

「はじめてのdnd-kit」シリーズ全4回の2回目です。

前回分はこちら

https://qiita.com/mkthrkw/items/447b88f87ae92dc1ad08

続きはこちら

https://qiita.com/mkthrkw/items/562f748e3002c1c3db48

https://qiita.com/mkthrkw/items/5db4e919d71980ae54ef



## 環境
下記のDocker開発環境にて行います。

https://qiita.com/mkthrkw/items/30115c9ac54c2204faef



## 基本コンポーネント

### イメージ
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/43a68092-1821-88f2-5535-4a688304450b.png">


### DndContext
・全体のドラッグアンドドロップコンテキストを提供する最上位コンポーネント
・ドラッグアンドドロップのロジック全体を管理する親コンポーネントで、この中でイベントリスナーや設定を行い、すべてのドラッグ操作を処理
・主にonDragEndやonDragStartなど、ドラッグイベントをハンドリングするためのプロパティを渡す

### SortableContext
・並べ替えが可能なアイテムのリストを囲むコンテキストコンポーネント
・ドラッグ可能なアイテムのコンテナとして機能し、並べ替え可能なグループを定義
・このコンテキスト内に並べ替えるアイテムを配置し、itemsというプロパティを渡し、その中のアイテムが並べ替え可能であることを示す

### useSortable
・個々のアイテムをドラッグアンドドロップ可能にするためのフック
・useDraggableとuseDroppableがまとまったイメージ
・ドラッグ操作中の状態も管理


## コンポーネントファイル作成

こちらのドキュメントに沿って単一コンテナの並べ替えを作成します。

https://docs.dndkit.com/presets/sortable


### インストール

利用するにはdnd-kit/sortableをインストールします。
```
npm install @dnd-kit/sortable
```

### Sortable.tsx
まずは```useSortable```を使ってドラッグアンドドロップするアイテム用のコンポーネントを作成します。

・最低限idはuseSortableの引数として渡す必要があります。
・```transition```：useSortableの引数として設定することで、durationの秒数や、[遷移タイミング関数](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function
)を設定できます。
使い所が難しいですが下記のような設定して変わった動きをさせることも出来ます。
```typescript
  } = useSortable({
    transition: {
      duration: 500,
      easing: 'cubic-bezier(0.25, 1, 1, -0.68)',
    },
    id: id
  });
```


・isDragging：ドラッグ中にTrueとなりstyle内に設定すれば、ドラッグ中だけ色を変えたり出来ます。
（```attributes```,```listeners```,```setNodeRef```,```transform```はuseDraggableと似ているので割愛）


```typescript
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { UniqueIdentifier } from '@dnd-kit/core';

type SortableProps = {
  children: React.ReactNode;
  id: UniqueIdentifier;
};

export function Sortable({children, id}: SortableProps) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({
    id: id
  });
  
  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    backgroundColor: isDragging ? 'green' : undefined,
  };
  
  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {children}
    </div>
  );
}
```


### SortableItem.tsx
ドラッグアンドドロップするアイテム自体のコンポーネントを作成。
サンプルなので、idを表示するだけのカードにします。

```typescript
import { UniqueIdentifier } from "@dnd-kit/core"

export function SortableItem ({itemId}: {itemId:UniqueIdentifier}) {
  return (
    <div className='w-full h-24 flex justify-center items-center border-2 border-dashed border-slate-300/50'>
      {`id:${itemId}`}
    </div>
  )
}
```

## 全体：サンプルの単一コンテナ

先に全体がこちらになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/876f6bf6-8d54-10c4-ee20-fed13404c9df.png)

```typescript:page.tsx
"use client";

import { useState } from 'react';
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
import { Sortable } from '@/util/dndkit/Sortable';
import { SortableItem } from '@/components/cards/SortableItem';
import { DndContext, DragEndEvent } from '@dnd-kit/core';

export default function Page() {
  const [items, setItems] = useState(
    {
      id: 'list-sample',
      title: 'List Sample',
      cards: [
        {id: 'card-1', title: 'Card 1'},
        {id: 'card-2', title: 'Card 2'},
        {id: 'card-3', title: 'Card 3'},
      ],
    },
  );

  function handleDragEnd(event: DragEndEvent) {
    const {active, over} = event;
    if (!over)return;
    if (active.id === over.id) return;
    const oldSortable = active.data.current?.sortable;
    const newSortable = over.data.current?.sortable;
    if (!oldSortable || !newSortable) return;
    
    setItems({
        ...items,
        cards: arrayMove(items.cards, oldSortable.index, newSortable.index),
      });
  }
  
  return (
    <div className='flex justify-center items-center w-screen h-screen'>
      <DndContext
        onDragEnd={handleDragEnd}
        id={items.id}
      >
        <SortableContext items={items.cards} key={items.id} id={items.id}>
          <div className='flex flex-col gap-4 border w-44 p-4'>
            {items.cards.map((card) => (
              <Sortable key={card.id} id={card.id}>
                <SortableItem itemId={card.id}/>
              </Sortable>
            ))}
          </div>
        </SortableContext>
      </DndContext>
    </div>
  );

}
```


## サンプルデータ
今回はシンプルなサンプルデータを使います。
```typescript
  const [items, setItems] = useState(
    {
      id: 'list-sample',
      title: 'List Sample',
      cards: [
        {id: 'card-1', title: 'Card 1'},
        {id: 'card-2', title: 'Card 2'},
        {id: 'card-3', title: 'Card 3'},
      ],
    },
  );
```

### コンポーネントの組み合わせ

<dl>
  <dt>DndContext</dt>
  <dd>
    一番外側に配置します。<br>
    後ほど記載しますが、ドロップ完了後の動作をonDragEndで指定します。
  </dd>
  <dt>SortableContext</dt>
  <dd>
    その次にSortableContextを配置します。<br>
    ここでitemsに並べ替えをするアイテムグループを渡して並べ替えアイテムを定義できます。
  </dd>
  <dt>div(column)</dt>
  <dd>
    リストを縦に並べたかったので、アイテムを並べる枠を作成。
  </dd>
  <dt>Sortable,SortableItem</dt>
  <dd>
    cards配列からmap関数で取り出して作成したSortable.tsxとSortableItem.tsxを使ってカードを配置。<br>
    反復して配置するため、keyとidにcard.idを指定。<br>
    表示用にSortableItemにもitemIdとしてcard.idを指定。
  </dd>
</dl>

```typescript
  return (
    <div className='flex justify-center items-center w-screen h-screen'>
      <DndContext
        onDragEnd={handleDragEnd}
        id={items.id}
      >
        <SortableContext items={items.cards}>
          <div className='flex flex-col gap-4 border w-44 p-4'>
            {items.cards.map((card) => (
              <Sortable key={card.id} id={card.id}>
                <SortableItem itemId={card.id}/>
              </Sortable>
            ))}
          </div>
        </SortableContext>
      </DndContext>
    </div>
  );
```


:::note warn
下記Issueに書かれているとおりServer側とClient側でaria-describedbyが相違してしまい
エラー「Warning: Prop `aria-describedby` did not match.
Server: "DndDescribedBy-1" Client: "DndDescribedBy-0"」が発生し、idを設定することで解消できました。
:::

https://github.com/clauderic/dnd-kit/issues/926






## handleDragEnd(function)について
ドロップ完了時の動作を設定します。

### イベントデータを取得

DragEndEventからactiveとoverを取得し、下記の時は即返却します。
・overがない：キャンセルされた
・active.idとover.idが同じ：同じ場所にドロップされた
```typescript
  function handleDragEnd(event: DragEndEvent) {
    const {active, over} = event;
    if (!over)return;
    if (active.id === over.id) return;
```

### 並べ替え前後のデータを取得

```active```,```over```はそれぞれ下記のようなデータになっています。

```json:active
{
    "id": "card-1",
    "data": {
        "current": {
            "sortable": {
                "containerId": "Sortable-0",
                "index": 0,
                "items": [
                    "card-1",
                    "card-2",
                    "card-3"
                ]
            }
        }
    },
    "rect": {
        "current": {
            "initial": null,
            "translated": null
        }
    }
}
```
```json:over
{
    "id": "card-2",
    "rect": {
        "width": 142,
        "height": 112,
        "top": 342.5,
        "bottom": 454.5,
        "right": 600,
        "left": 458
    },
    "data": {
        "current": {
            "sortable": {
                "containerId": "Sortable-0",
                "index": 1,
                "items": [
                    "card-1",
                    "card-2",
                    "card-3"
                ]
            }
        }
    },
    "disabled": false
}
```
上記から、```data.current.sortable```を取得します。
```typescript
    const oldSortable = active.data.current?.sortable;
    const newSortable = over.data.current?.sortable;
    if (!oldSortable || !newSortable) return;
```


### 新しいリストとしてitemsを更新

上記で取得したSotableからIndexを取得して```arrayMove```で入れ替えた配列を取得
```arrayMove```は、配列・移動元のインデックス・移動先のインデックスの3つを渡して、入れ替えた配列を返してくれます。

スプレッド構文でitemsを展開し、入れ替えた配列でcardsを上書きし、seItemsでセットします。

```typescript
    setItems({
        ...items,
        cards: arrayMove(items.cards, oldSortable.index, newSortable.index),
      });
```

#### arrayMoveについて

中身はArray.spliceを使って、削除と追加をやっているものでした。

```typescript
/**
 * Move an array item to a different position. Returns a new array with the item moved to the new position.
 */
export function arrayMove<T>(array: T[], from: number, to: number): T[] {
  const newArray = array.slice();
  newArray.splice(
    to < 0 ? newArray.length + to : to,
    0,
    newArray.splice(from, 1)[0]
  );

  return newArray;
}
```


https://github.com/clauderic/dnd-kit/blob/master/packages/sortable/src/utilities/arrayMove.ts



## テスト
無事入れ替えが出来ました。

![入れ替え.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/80c4d1c4-6b2f-e6a1-c494-f4b7ad37b4a8.gif)

## つづき

https://qiita.com/mkthrkw/items/562f748e3002c1c3db48

## さいごに
・単一コンテナで順番を並べ替えるだけならシンプルですがこちらで十分そうです。
・次回はもう少し難しそうな複数コンテナの並び替えに挑戦したいと思います。

## 参考

https://docs.dndkit.com/presets/sortable