React DnD 超入門 〜Drag Around 編〜

# はじめに
こんにちは、よわよわエンジニアのshungikuです！

今回はReactでドラッグ＆ドロップを実現するためのライブラリである「React DnD」についての入門記事になります。

[本家のページ](https://react-dnd.github.io/react-dnd/docs/tutorial)にもチュートリアルはありますが、チェスを題材にしていて謎に難しいです。（てかチェスとかよう知らんし！）
それに、自由な位置にドラッグ＆ドロップする方法についてはチュートリアルがなかったため、自分が使ってみて得た知見をまとめていきたいと思います。

![drag.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1047806/68e6469b-61a5-d7bf-0d22-d78dbcf61321.gif)


## この記事の対象者
- React DnD を使ってみたいが、重厚長大なチュートリアルしかないためよう分からんという人
- React DnD を使って自由な位置にドラッグ＆ドロップする方法を知りたい人

# React DnDの使い方

## パッケージのインストール
まずは2つのパッケージをインストールしましょう。
```
npm install react-dnd react-dnd-html5-backend
```

インストールが完了したら、早速React DnDを使っていきましょう。

## DndProviderでラップ

まずは、ドラッグ＆ドロップを使いたい範囲を`DndProvider`でラップします。
本稿では、`Example`というコンポーネントに実装例を書いていくので、そのコンポーネントがあるという体で以下のようにラップします。

ちなみに、`backend` というpropsに`HTML5Backend`を渡していますが、一旦魔法だと思ってスルーしましょう。

```react:App.tsx
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Example } from "./Example";

export default function App() {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>
        <Example />
      </DndProvider>
    </div>
  );
}
```


## 見た目の作成

続いて、ドラッグ対象の箱と、ドロップ可能範囲の枠を作ります。
`Example.tsx`ファイルを作ってそこに書いていきましょう。

箱の位置である`top`と`left`は`useState`でStateとして定義します。
後々、ドロップされた時に`setBox`で位置情報を更新します。

```react:Example.tsx
import React from "react";

const ContainerStyle: React.CSSProperties = {
  width: 500,
  height: 500,
  backgroundColor: "silver"
};

const BoxStyle: React.CSSProperties = {
  position: "absolute",
  border: "1px dashed gray",
  backgroundColor: "white",
  padding: "0.5rem 1rem",
  cursor: "move"
};

type Box = {
  top: number;
  left: number;
};

export const Example: React.FC = () => {
  const [box, setBox] = React.useState<Box>({ top: 20, left: 20 });
  return (
    <div style={ContainerStyle}>
      <div style={{ ...BoxStyle, top: box.top, left: box.left }}>
        Drag me around
      </div>
    </div>
  );
};
```

こんな感じの箱と枠ができます。

<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1047806/762d2171-51db-8061-709b-4620a3173259.png" />

## ドラッグ制御の実装 ~useDrag~

ドラッグするコンポーネントに対して、`useDrag`というhooksを使います。

```react:Example.tsx
  const [collected, drag, dragPreview] = useDrag(
    {
      type: "box",
      item: { top: box.top, left: box.left }
    },
    [box]
  );
```

### 引数
#### 第一引数
第一引数には動作仕様を定義するためのオブジェクト、もしくはオブジェクトを返す関数を渡します。例ではオブジェクトを渡しています。
オブジェクトのkeyはいくつか種類があるのですが、ここではまず`type`と`item`の２つだけ紹介します。

`type`には`DndProvider`内で一意のIDを記述します。（例では`box`）
後々説明しますが、ドロップ側でも同じIDを指定することで、ドラッグ側とドロップ側で連携が取れるようになります。

`item`にはドラッグ側とドロップ側でやりとりしたい情報を記述します。
例では、`box` の `top` と `left` の情報を渡しています。

#### 第二引数

第二引数には依存関係を持つ変数を配列で渡します。useMemoなどと同様の使い方ですね。
例では、State変数である`box`に依存していますので、`box`を配列に入れています。
これを指定しないと正常に動作しなくなるので、興味のある方は試してみてください。

### 戻り値

戻り値は配列で３つ返ってきますが、まずは２つ目（例では`drag`）だけにフォーカスして、他２つは一旦スルーしてください。

２つ目の戻り値をドラッグ対象の要素に`ref`で渡してあげます。

```react:Example.tsx
  return (
    <div className='w-[500px] h-[500px] bg-gray-200'>
      {/* ↓ refで渡す部分を追加 ↓ */}
      <div ref={drag} style={{...boxStyle, top: box.top, left: box.left}}>hello</div>
    </div>
  )
```

## ドロップ制御の実装 ~useDrop~

ドロップするコンポーネントに対しては、`useDrop`というhooksを使います。

```react:Example.tsx
  const [collectedProps, drop] = useDrop(
    () => ({
      accept: "box",
      drop(item: Box, monitor) {
        const delta = monitor.getDifferenceFromInitialOffset() as XYCoord;
        const left = Math.round(item.left + delta.x);
        const top = Math.round(item.top + delta.y);
        setBox({ top, left });
        return undefined;
      }
    }),
    []
  );
```

### 引数
#### 第一引数
第一引数には動作仕様を定義するためのオブジェクト、もしくはオブジェクトを返す関数を渡します。例ではオブジェクトを渡しています。
オブジェクトのkeyはいくつか種類があるのですが、ここではまず`accept`と`drop（item, monitor）`の２つだけ紹介します。

`accept`には`useDrag`で指定したIDと同じものを指定します。（例では`box`）
これにより、ドラッグ側と連携が取れるようになります。

`drop（item, monitor）`にはドロップ時に発火させる関数を指定します。
関数は２つの引数を持ちます。

１つ目（例では`item`）には、`useDrag`の`item`で指定した値が渡ってきます。

２つ目（例では`monitor`）にはドラッグ&ドロップ中の状態をモニターするためのオブジェクトが渡ってきます。詳細な説明は一旦省きますが、様々な便利メソッドを具備しています。

ここで、`drop（item, monitor）`における処理を詳しく見ていきましょう。

```React: Example.tsx
      drop(item: Box, monitor) {
        const delta = monitor.getDifferenceFromInitialOffset() as XYCoord;
        const left = Math.round(item.left + delta.x);
        const top = Math.round(item.top + delta.y);
        setBox({ top, left });
        return undefined;
      }
```


例では、`monitor.getDifferenceFromInitialOffset()`を叩くことで、ドラッグ前と後の位置情報の差分を取得しています。
例えば、左や上に動かしたら x,y座標差分としてそれぞれ負の値が取得され、右や下に動かしたら正の値が取得されます。

その後、ドラッグ側から渡ってきた初期位置（例では、`item`の`left`と`top`）に対して取得した差分を加算することで、ドロップ後の位置を算出し、`setBox`でState変数を更新します。

これにより、State変数の`top`と`left`の値が更新され、再描画の処理が走り、対象要素がドロップ後の位置に描画されます。

ちなみに、`drop（item, monitor）`の戻り値には`undefined`または何らかのオブジェクトを指定します。
オブジェクトを返すときのユースケースについては、また別の記事で紹介したいと思います。

#### 第二引数

第二引数には依存関係を持つ変数を配列で渡します。
例では、特に依存変数はないため空配列を渡しています。

### 戻り値

戻り値は配列で2つ返ってきますが、まずは２つ目（例では`drop`）だけにフォーカスして、他は一旦スルーしてください。

２つ目の戻り値をドロップ対象の要素にrefで渡してあげます。

```React:Example.tsx
  return (
    <div ref={drop} className='w-[500px] h-[500px] bg-gray-200'>
      <div ref={drag} style={{...boxStyle, top: box.top, left: box.left}}>hello</div>
    </div>
  )
```

# 完成物

ここまでの全体のコードを掲載します。
```tsx:App.tsx
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Example } from "./Example";

export default function App() {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>
        <Example />
      </DndProvider>
    </div>
  );
}
```

```tsx:Example.tsx
import React from "react";
import { useDrag, useDrop, XYCoord } from "react-dnd";

const ContainerStyle: React.CSSProperties = {
  width: 500,
  height: 500,
  backgroundColor: "silver"
};

const BoxStyle: React.CSSProperties = {
  position: "absolute",
  border: "1px dashed gray",
  backgroundColor: "white",
  padding: "0.5rem 1rem",
  cursor: "move"
};

type Box = {
  top: number;
  left: number;
};

export const Example: React.FC = () => {
  const [box, setBox] = React.useState<Box>({ top: 20, left: 20 });

  const [collected, drag, dragPreview] = useDrag(
    {
      type: "box",
      item: { top: box.top, left: box.left }
    },
    [box]
  );

  const [collectedProps, drop] = useDrop(
    () => ({
      accept: "box",
      drop(item: Box, monitor) {
        const delta = monitor.getDifferenceFromInitialOffset() as XYCoord;
        const left = Math.round(item.left + delta.x);
        const top = Math.round(item.top + delta.y);
        setBox({ top, left });
        return undefined;
      }
    }),
    []
  );

  return (
    <div ref={drop} style={ContainerStyle}>
      <div ref={drag} style={{ ...BoxStyle, top: box.top, left: box.left }}>
        Drag me around
      </div>
    </div>
  );
};

```

# おわりに
出来るだけ軽量なサンプルコードでReact DnDの使い方を説明してみました。
取っ掛かりとして参考になれば幸いです。

また、"超入門"ということで出来るだけシンプルな内容に留めたかったため、いろいろな箇所の説明を端折っています。
より詳細な使い方については、気が向いたらまた別記事で書いていけたらと思っています。


# 参考
- [React DnD](https://react-dnd.github.io/react-dnd/about)
- [React DnDを使ったので知見をまとめた - hey Product Blog](https://tech.hey.jp/entry/2021/03/09/131638)