dnd-kitで1つのコンテナ内で要素を縦に並び替え

## 0. この記事について
自己学習でドラッグ&ドロップできるtodoアプリをNext.jsで作成しています。
その中でdnd-kitを初めて使用しました。
一旦実装はしたものの、正直があまり理解しきれてない部分も多いので、
1つのコンテナ内で要素を縦に並び替えるシンプルな機能を再実装しながら、基本機能を復習します。

## 1. dnd-kitとは
ドラッグ & ドロップ機能を実装できるReactのためのライブラリ。

### 特徴
主な特徴は[こちら](https://docs.dndkit.com/)にもありますが、私も実装の中で下記のようなメリットは実感しました。

- **軽量**
組み込みの React 状態管理とコンテキストを中心に構築されているため、ライブラリがスリム
ライブラリのコアは縮小すると約 10 KB になり軽量のため、パフォーマンスに与える影響を抑えられる

- **外部の依存関係がない**
他のパッケージの更新やバージョン不整合などを気にしなくて良い

- **拡張性が高い**
アニメーション、トランジション、動作、スタイルなど、幅広くカスタマイズできるので、必要に応じて独自の機能を追加可能

### 今回dnd-kitを採用した理由
ドラッグ & ドロップのライブラリ自体はdnd-kit以外にもあるのですが、
[npm trends](https://npmtrends.com/@dnd-kit/core-vs-react-beautiful-dnd-vs-react-dnd-vs-react-draggable)では現時点でdnd-kitが一番多くDLされています。
DL数が多い= そもそもライブラリとして勢いがある、またその分ドキュメントや技術記事が充実しており初学者にも取りかかりやすいのでは、と思いました。


## 2. 基本的な使い方（1つのコンテナ内で要素を縦に動かす）

### 前提
今回は下記のような1つのコンテナ内で複数のアイテムを縦に動かせる機能を実装します。

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3876203/69a1d0ff-5933-4a98-95a6-09a53c9a89f0.gif)

かなりシンプルですが、ディレクトリ構造は下記になります。
（App routerを使用したので、`app`配下に作成していきます）
```
src/app/
　├ page.tsx
　└ component
　　└ DragItem.tsx
```
 
### 1. コアライブラリのインストール
まず下記コマンドで自身のReactプロジェクトにdnd-kitのコアライブラリをインストールします。
```
npm install @dnd-kit/core
```

### 2. DndContextを用意する
DndContextはアプリケーション内のドラッグ＆ドロップの状態を管理し、必要な情報を子コンポーネントに渡します。
つまりドラッグ ・ドロップ可能要素は全てDndContextに囲まれている必要があるので、
まずはこちらを用意します。


```tsx:page.tsx:
'use client';

import React from 'react';
import { DndContext } from '@dnd-kit/core';

export default function Home() {
  return (
    <DndContext>
       {/* ここにドラッグ・ドロップ可能要素が入る */}
    </DndContext>
  );
}
```
:::note info
今回の機能はClient Component になるので、1行目に`'use client';`を記載します。
:::



### 3. SortableContextを用意する
要素のソートを行う為に必要です。ソート対象の要素は`SortableContext`に囲まれている必要があるので、`DndContext`の配下に用意します。

利用する際は下記`dnd-kit/sortable`のインストールが必要です。
```
npm install @dnd-kit/sortable
```

インストールできたらDndContextの配下に追加します。

```tsx:page.tsx
'use client';

import React, { useState } from 'react';
import { DndContext } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';

export default function Home() {
  return (
    <>
      <DndContext>
        <SortableContext>
             {/* ここにドラッグ可能要素が入る */}
        </SortableContext>
      </DndContext>
    </>
  );
}
```


### 4. items用の配列を用意
SortableContextにはドラッグ対象要素のidを最低限渡す必要があります。
今回は複数のアイテムを用意したいので、シンプルに3つのidを用意し、また各要素に表示するテキストが入った配列を静的に追加しました。
後ほどこの配列は更新するので`useState`で定義しておきます。

定義した`items`は`SortableContext`の`items`に渡しておきます。
```tsx:page.tsx
import React, { useState } from 'react';

{/* 中略 */}

export default function Home() {
  const [items, setItems] = useState([
    {
      id: 1,
      title: 'title1'
    },
    {
      id: 2,
      title: 'title2'
    },
    {
      id: 3,
      title: 'title3'
    }
  ]);

  return (
    <>
      <DndContext>
        <SortableContext items={items}>
```

### 5. useSortableの追加
並び替え対象の要素に使用するフックです。


```tsx:DragItem.tsx
import React, { MouseEventHandler } from 'react';
import { useSortable } from "@dnd-kit/sortable";

export default function DragItem ({ id, title }) {
    const { attributes, listeners, setNodeRef, transform } = useSortable({ id });

    const style = transform ? {
        transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
    } : undefined;

    return (
        <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
		  {title}
        </div>
    );
}
```
#### `useSortable`から受け取る各プロパティについて
- **attributes**
ARIAやrole属性といったWAI-ARIAに当たる属性を渡してくれます。

- **setNodeRef**
`setNodeRef`はドラッグ＆ドロップの対象となる要素を追跡し、
どの要素がドラッグされているのか、どこにドロップされるのかを伝えるために使われます。

- **listeners**
マウスイベントやタッチイベントを検知するlistenerが渡ります。
これがないとドラッグを検知せず、要素が動かせなくなります。

- **style**
ドラッグ対象要素が元々いた場所からの座標を返します。
ドラッグすると下記のようなオブジェクトが渡ってきます。
`{x: -130, y: 114, scaleX: 1, scaleY: 1}`


### 6. DragItemの組み込み
DragItemコンポーネントが完成したらDragItemも組み込みます。
DragItemは複数配置したいのでmapを使用して、手順4で用意しておいた`id`と`title`を受け取るようにします。

```tsx:page.tsx
  import DragItem from './component/DragItem';

  {/* 中略 */}

  return (
    <>
      <DndContext>
        <SortableContext items={items}>
            {items.map((item) => (
              <DragItem key={item.id} id={item.id} title={item.title} />
            ))}
        </SortableContext>
      </DndContext>
    </>
  );
```

ここまで実装するとドラッグ自体は可能なのですが、並び替えをすることはまだできません。
onDragEndの定義が必要です。

### 7. onDragEnd処理を定義
onDragEndはドラッグしマウスを離した際の処理を定義します。
今回は`itemDragEnd`の関数名にし、下記のように定義しました。

````tsx:page.tsx
import { DndContext, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, arrayMove } from '@dnd-kit/sortable';

{/* 中略 */}

  const itemDragEnd= (event : DragEndEvent) => {
      const { active, over } = event;
      if (!over == null || active.id === over.id) return;

      const oldIndex = over.data.current?.sortable?.index;
      const newIndex = active.data.current?.sortable?.index;
      const newItems = arrayMove(items, oldIndex, newIndex);
      setItems(newItems);
  }
````

`event`で受け取れるオブジェクトに`active` / `over`というキーがあり、それぞれ下記情報を受け取ります。
- `active`: ドラッグ中の要素の情報
- `over`: ドラッグ要素が接触した要素の情報

また`arrayMove`はアイテムの順序を変更する関数で、`@dnd-kit/sortable`からimportできます。

元の配列と、ドラッグ要素の変更前のindex、変更後のindexを渡すことで、新しい配列を作成する関数です。
その配列を`useState`（今回のソースだと`setItems`）を通して更新してあげることで、ドラッグ後の状態を実現できる、という流れです。

### 8. onDragEnd処理をDndContextへ追加
実装した`itemDragEnd`をDndContextの`onDragEnd`へ渡します。
```tsx:page.tsx
{/* 中略 */}

return (
    <>
      <DndContext onDragEnd={itemDragEnd}>
```
### 9. 完成
これで各要素をドラッグ & ドロップができるようになっていると思います。


## +α
### ドラッグ要素にonClick属性をつけたい
そのままcomponentに`onClick`を追加してもうまく動きません。
この場合は`sensors`を使用して、「何px動かしたらドラッグを判定するか」を定義します。

まず`@dnd-kit/core`から`useSensor`、 `useSensors`、`MouseSensor`をimportします。

```tsx:page.tsx
import { DndContext, DragEndEvent, useSensor, useSensors, MouseSensor } from '@dnd-kit/core';
```

importしたsensor達は下記のように記述します。
これでマウスにより対象要素が5px（= `distance`に設定した数値）以上動いた場合 = ドラッグされたと認識するようになります。
```tsx:page.tsx
  const sensors = useSensors(
    useSensor(MouseSensor, { activationConstraint: { distance: 5 } })
  )
```

作成した`sensors`は`DndContext`に渡します。
これでclickイベントにも反応してくれるようになります。
```tsx:page.tsx
  return (
    <>
      <DndContext onDragEnd={itemDragEnd} sensors={sensors}>
```

### ドラッグ中のみ該当要素のstyleを変えたい

ドラッグ中であることが分かりやすいように見た目を変えることも可能です。
調べてみると`DragOverlay`なども紹介されていましたが、今回はドラッグ中の要素は背景色を変えたいだけなのでシンプルに実装しました。


`DragItem`の`useSortable`から`isDragging`を追加で受け取ります。
名前の通り、ドラッグ可能要素がドラッグされていると`isDragging`は`true`を返します。
```tsx:DragItem.tsx
  const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id });
```

`isDraging`でstyleを分岐させます。
これでドラッグ中の要素のみ背景色を変更することができます。
```tsx:DragItem.tsx
  const style = transform ? {
    transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
    backgroundColor: isDragging ? '#ccc' : '#fff',
    zIndex: isDragging ? 100 : 1,
  } : undefined;
```

:::note info
余談ですが、背景色を変えたところ、ドラッグ中の要素が他の要素の下に回り込んでしまうことに気がついたので、ドラッグ中の要素が常に上に来るよう`z-index`も追加で指定しました。
:::

### 要素の該当部分をドラッグした時のみ動かしたい

ドラッグ要素にハンドルを用意し、そこをドラッグした際のみ要素が動かせるようにします。
`DragItem.tsx`の`useSortable`から`setActivatorNodeRef`を受け取ります。
```tsx:DragItem.tsx
const { attributes, listeners, setNodeRef, transform, isDragging, setActivatorNodeRef } = useSortable({ id });
```

今回は`DragHandle`componentを作成したので、こちらをドラッグした時のみ反応するようにします。（`DragHandle`の中身はハンドルの見た目のみです）
そのラッパー要素に先ほどの`setActivatorNodeRef`と、元々外側の`div`にあった`{...listeners}`、`{...attributes}`をハンドル側に移動します。
```tsx:DragItem.tsx
return (
    <div ref={setNodeRef} style={style} onClick={onClick}>
		 {title}
        <div ref={setActivatorNodeRef} {...listeners} {...attributes} >
          <DragHandle />
        </div>
     </div>
);
```

:::note info
`{...listeners}`だけを移動しても動きますが、公式ドキュメント（[こちら](https://docs.dndkit.com/api-documentation/draggable#flexibility)）に記載があるように、`{...listeners}`がアタッチされている同じDOMにも、`{...attributes}`をアタッチする必要があるようです。
:::

ちなみに上記で紹介した方法を使って、ドラッグ中は`background-color`を変更して、各アイテムにハンドルを用意した際の下記のような挙動になります。
ハンドル以外をドラッグした際はアイテムが動かず、右のドット箇所のみドラッグ可能になります。
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3876203/ffa5042b-cd22-4b26-bc36-2f88186f608f.gif)


## まとめ・感想
今回初めてdnd-kitを使用してみましたが、思っていたよりもシンプルにドラッグ&ドロップの機能自体は実装することができました。
今回は復習として、1つのコンテナ内で要素を縦に動かすのみの挙動でしたが、実際に自己学習で作成した機能は複数コンテナ間での要素を動かす挙動なので、こちらもまたQiitaにまとめたいと思います。


## 参考
https://docs.dndkit.com/

https://docs.dndkit.com/api-documentation/context-provider

https://docs.dndkit.com/api-documentation/draggable/usedraggable

https://docs.dndkit.com/api-documentation/draggable

https://qiita.com/mkthrkw/items/278369e2b596e657eaa5

https://techlab.q-co.jp/articles/60/

https://zenn.dev/castingone_dev/articles/dndkit_sortalbe#%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%81%AE%E3%81%A4%E3%81%BE%E3%81%BF%E9%83%A8%E5%88%86%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88