🚀

Reactのドラッグ&ドロップライブラリ「dnd kit」を使ってかんばんボードを作る

2022/12/20に公開

みなさん、こんにちは!アルダグラムでエンジニアをしている大木です。

今回は、Reactのドラッグ&ドロップライブラリである「dnd kit」を使ってかんばんボードを作ってみようかと思います。

きっかけ

アルダグラムが提供している、「KANNA」はアプリに限らずWebでのサービス提供もしています。とある新規機能で、ドラッグ&ドロップが可能な画面が作ることになりました。何か使えそうなライブラリはないかなぁと探していたのがdnd kitを知ったきっかけです。

Reactですと、

なんかが有名どころかと思います。

個人的には「React Dndはなんかとっつきにくそう」という第一印象を持ったのと、react-beautiful-dndに関しては今後のメンテナンスに関する課題があるといったところで導入を躊躇っていました。そこで出会ったのがこのdnd kitです。

サンプルを触ってみる

公式にいくつかサンプルが存在しており、自由に触ることができます。

下記のリンクにあるものはまさにかんばんボードのような動きを体験することができます。

https://master--5fc05e08a4a65d0021ae0bf2.chromatic.com/?path=/story/presets-sortable-multiple-containers--basic-setup

実装してみる

サンプルもサクサク動いて良さそうではあるのですが、コードを読んでみるとこちらも少し実装が難しそうな印象を持ちました。「実際に手を動かしてみないとわからない!」というところで簡単なかんばんボードを作ってみたいと思います。

今回実装してみたサンプルは以下から確認することができます。(デザインに関してはお察しください)

では実装していきましょう!

1. インストール

今回は dnd-kit/corednd-kit/sortable の2つをインストールします。

npm install @dnd-kit/core @dnd-kit/sortable

2. ドラッグできるカードの作成

実際にドラッグをするためのカード(かんばんボードでいうとタスク)のコンポーネントを作成します。

useSortable を利用して、ドラッグ可能なコンポーネントを定義します。

src/Card.tsx
import { FC } from "react";
import { CSS } from "@dnd-kit/utilities";
import { useSortable } from "@dnd-kit/sortable";

type CardType = {
  id: string;
  title: string;
};

const Card: FC<CardType> = ({ id, title }) => {
  const { attributes, listeners, setNodeRef, transform } = useSortable({
    id: id
  });

  const style = {
    transform: CSS.Transform.toString(transform)
  };

  return (
    // attributes、listenersはDOMイベントを検知するために利用します。
    // listenersを任意の領域に付与することで、ドラッグするためのハンドルを作ることもできます。
    <div ref={setNodeRef} {...attributes} {...listeners} style={style}>
      <div id={id}>
        <p>{title}</p>
      </div>
    </div>
  );
};

export default Card;

参考:
https://docs.dndkit.com/presets/sortable/usesortable

3. ドロップ可能なカラムの作成

カードを置くことのできるカラム(列部分)のコンポーネントを作成します。

useDroppable を利用して、ドロップ可能なコンポーネントを定義します。

src/Column.tsx
import { FC } from "react";
import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
import { useDroppable } from "@dnd-kit/core";
import Card, { CardType } from "./Card";

export type ColumnType = {
  id: string;
  title: string;
  cards: CardType[];
};

const Column: FC<ColumnType> = ({ id, title, cards }) => {
  const { setNodeRef } = useDroppable({ id: id });
  return (
    // ソートを行うためのContextです。
    // strategyは4つほど存在しますが、今回は縦・横移動可能なリストを作るためrectSortingStrategyを採用
    <SortableContext id={id} items={cards} strategy={rectSortingStrategy}>
      <div ref={setNodeRef}>
        <p>{title}</p>
        {cards.map((card) => (
          <Card key={card.id} id={card.id} title={card.title}></Card>
        ))}
      </div>
    </SortableContext>
  );
};

export default Column;

参考
https://docs.dndkit.com/presets/sortable/sortable-context

4. ボード部分の作成

今まで定義してきた、ドラッグ&ドロップをするためのコンポーネントを操作可能にするために DndContext を定義します。

sensors、collisionDetectionの説明に関しては長くなってしまうため割愛します。詳しくは公式のドキュメントをご参考ください。

src/App.tsx
import "./styles.css";
import {
  closestCorners,
  DndContext,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors
} from "@dnd-kit/core";
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
import Column, { ColumnType } from "./Column";

export default function App() {
  // 仮データを定義
  const columns: ColumnType[] = [...];

  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates
    })
  );
  return (
    // 今回は長くなってしまうためsensors、collisionDetectionなどに関しての説明は省きます。
    <DndContext sensors={sensors} collisionDetection={closestCorners}>
      <div className="App">
        {columns.map((column) => (
          <Column
            key={column.id}
            id={column.id}
            title={column.title}
            cards={column.cards}
          ></Column>
        ))}
      </div>
    </DndContext>
  );
}

参考:
DndContextについてはこちら
https://docs.dndkit.com/api-documentation/context-provider
Sensorsについてはこちら
https://docs.dndkit.com/api-documentation/sensors
Collision Detectionについてはこちら
https://docs.dndkit.com/api-documentation/context-provider/collision-detection-algorithms

この時点で、Cardのコンポーネントがドラッグできることが確認できるかと思います。簡単にデザインを付け足して、以下のような見た目に変えてみました。

5. ドラッグ&ドロップのイベントを検知して、カードの並び替えを行う

現状、カードのドラッグはできますが、カードを放すと元の位置に戻るだけ、かつ横の移動ができないためその処理を追加していこうかと思います。

DndContext には様々なEventが用意されており、今回はそちらを利用して並び替えの処理を行いたいと思います。(dnd kitはTypescriptのサポートもあるために、型安全に書くことができます。素敵ですね。)

  • onDragEnd: その名の通り、ドラッグし終えた時に発火されるイベントです
  • onDragOver: ドラッグ可能なものが Droppable の領域を超えた時に発火されるイベントです
src/App.tsx
import {
  closestCorners,
  DndContext,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  DragEndEvent,
  DragOverEvent
} from "@dnd-kit/core";

export default function App() {
  const handleDragEnd = (event: DragEndEvent) => {
    // 今回のサンプルでは、同カラム内での並び順変更を実装しています。
    ...
  }
  const handleDragOver = (event: DragOverEvent) => {
    // 今回のサンプルでは、別カラムへ移動するための処理を実装しています。
  }
  return (
    <DndContext
      onDragEnd={handleDragEnd}
      onDragOver={handleDragOver}
    >
    ...
    </DndContext>
  )
}

具体的な実装に関しては、CodeSandbox内の src/App.tsx をご覧ください。

https://codesandbox.io/s/dnd-kit-kanban-board-1df69n?file=/src/App.tsx:1503-1556

6. 完成!

簡単なサンプルではありますが、縦・横の移動が可能なかんばんボードを作成することができました。CodeSandboxにあるコードにはいくつか改善点があるかとは思いますが、1つの実装例として捉えていただければと思います。

まとめ

コードベースの記事になってしまいましたが、ここまでご覧いただきありがとうございました。

  • 実際に作ってみると、案外容易にドラッグ&ドロップの実装ができたこと
  • 並び替えなどの処理が、 DndContext に集約できる

というようなところから、dnd kitが1つの選択肢になり得るなと感じました。今回、KANNAではdnd kitの採用は見送りましたが、もし本番環境などで運用している方がいらっしゃればより詳しい所感などをお聞きしてみたいです。

ここまでご覧いただきありがとうございました!!

アルダグラム Tech Blog

Discussion