"react-beautiful-dnd" で実装する React の Drag&Drop with TypeScript, 関数コンポーネント

## はじめに

React で自前の Drag&Drop (DnD) を実装するのは、なかなか大変だと思います。
ライブラリを探していたところ、Atlassian 製の [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) というライブラリがよさそうだったので使ってみての所感を書きます。

## 公式サンプル
[縦方向リストの DnD](https://codesandbox.io/s/k260nyxq9v)
[横方向リストの DnD](https://codesandbox.io/s/mmrp44okvj)
[関数コンポーネントで実装](https://codesandbox.io/s/zqwz5n5p9x)
[2カラム間の DnD](https://codesandbox.io/s/ql08j35j3q)

ヌルヌル動いて気持ち〜〜〜

![dnd_sample_official.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435654/f377aa97-4b5b-1e74-0474-ecde916de498.gif)

## 手元で実装してみる
### バージョン
"typescript": "^3.7.3",
"react": "^16.12.0",
"[react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)": "^12.2.0",
"[styled-components](https://www.styled-components.com/)": "^4.4.1"

### 作ったのはこれ
![dnd_sample_kats.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435654/7261cd92-f5bb-ad28-6e3c-d04e8638f442.gif)
（見た目はほぼサンプルと同じだねとか、、聞こえてますよ、、ええ）

↓ディレクトリ構成など分かりやすいようにアレンジしてあるので参考にしてみてください↓
https://github.com/kk-web/react-beautiful-dnd_sample

### インストール
※ React まわりの構築は割愛します。

まずは、[モジュール](https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/about/installation.md)をインストール！

````sh
npm install --save react-beautiful-dnd
````

### 元になるリストを作る

```tsx:src/App.tsx
import React from "react";
import { ItemType } from "./types";
import List from "./List";

const App = () => {
  const initial: ItemType[] = Array.from({ length: 10 }, (v, k) => k).map(k => {
    return {
      id: `id-${k}`,
      content: `Item ${k}`
    };
  });
  return <List items={initial} />;
};

export default App;
```

```tsx:src/types.ts
export type ItemType = {
  id: string;
  content: string;
};
```

```tsx:src/List.tsx
import React from "react";
import Item from "./Item";

const List = ({ items }) => (
  <>
    {items.map(item => (
      <Item item={item} key={item.id} />
    ))}
  </>
);

export default List;
```

```tsx:src/Item.tsx
import React from "react";
import styled from "styled-components";

const StyledItem = styled.div`
  width: 200px;
  border: 1px solid grey;
  margin-bottom: 8px;
  background-color: lightblue;
  padding: 8px;
`;

const Item = ({ item }) => {
  return <StyledItem>{item.content}</StyledItem>;
};

export default Item;
```

### 型定義
[ドキュメントの型定義セクション](https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/types.md)には、

> *Typescript
If you are using TypeScript you can use the community maintained [DefinitelyTyped type definitions](https://www.npmjs.com/package/@types/react-beautiful-dnd). Installation instructions.*

とあります。[型定義モジュール](https://www.npmjs.com/package/@types/react-beautiful-dnd)もインストールしておきましょう。

````sh
npm install --save @types/react-beautiful-dnd
````

> *Here is an [example written in typescript](https://github.com/abeaudoin2013/react-beautiful-dnd-multi-list-typescript-example).*

[サンプル](https://github.com/abeaudoin2013/react-beautiful-dnd-multi-list-typescript-example)も用意されています！

### DnD 実装

```tsx:src/App.tsx
import React, { useState } from "react";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { ItemType } from "./types";
import List from "./List";

const reorder = (
  list: ItemType[],
  startIndex: number,
  endIndex: number
): ItemType[] => {
  const result = Array.from(list);
  const [removed] = result.splice(startIndex, 1);
  result.splice(endIndex, 0, removed);

  return result;
};

const App = () => {
  const initial: ItemType[] = Array.from({ length: 10 }, (v, k) => k).map(k => {
    return {
      id: `id-${k}`,
      content: `Item ${k}`
    };
  });
  const [state, setState] = useState({ items: initial });

  const onDragEnd = result => {
    if (!result.destination) {
      return;
    }

    if (result.destination.index === result.source.index) {
      return;
    }

    const items = reorder(
      state.items,
      result.source.index,
      result.destination.index
    );

    setState({ items });
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="list">
        {provided => (
          <div ref={provided.innerRef} {...provided.droppableProps}>
            <List items={state.items} />
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

export default App;
```

↓変更ありません

```tsx:src/types.ts
export type ItemType = {
  id: string;
  content: string;
};
```

```tsx:src/List.tsx
import React from "react";
import Item from "./Item";

const List = React.memo<{ items }>(({ items }) => (
  <>
    {items.map((item, index: number) => (
      <Item item={item} index={index} key={item.id} />
    ))}
  </>
));

export default List;
```

```tsx:src/Item.tsx
import React from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";

const StyledItem = styled.div`
  width: 200px;
  border: 1px solid grey;
  margin-bottom: 8px;
  background-color: lightblue;
  padding: 8px;
`;

const Item = ({ item, index }) => {
  return (
    <Draggable draggableId={item.id} index={index}>
      {provided => (
        <StyledItem
          ref={provided.innerRef}
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          {item.content}
        </StyledItem>
      )}
    </Draggable>
  );
};

export default Item;
```

### Server Side Rendering
Next.js で実装したときにつまづきました、、

Drag できなくて、console を見てみると
>react-beautiful-dnd A setup problem was encountered.
\> Invariant failed: Draggable[id: id-9]: Unable to find drag handle👷‍ This is a development only message. It will be removed in production builds.

とエラーが出ていました。
[ドキュメント](https://github.com/atlassian/react-beautiful-dnd#api-%EF%B8%8F)を見てみると

> *API 🏋
(中略)
[resetServerContext()](https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/reset-server-context.md) - Utility for server side rendering (SSR)*

と記載が！
SSR の場合は、任意の場所でこの関数を実行しましょう！

## おわりに
DnD を自作で実装しようとすると手間がかかりますが、react-beautiful-dnd を使えば数行の実装用関数と state や props を設定するだけで、ヌルヌル動くものができるのはありがたいですね！
公式サンプルではコンポーネントが1つのファイルにまとめられていますが、今回の実装のようにリストやアイテムで分解すれば、既存のリストにも適用しやすいのではないかと思います。