React で DnD するなら、dnd kit
久しぶりの記事になってしまいました。
最近、React でドラッグ&ドロップ(以下、DnD)を実装する機会があり、いい感じのライブラリないか探していました。
React の DnD ライブラリというと、以下の2つが代表的かと思います。
react-dnd は個人的になんかとっつきにくく、react-beautiful-dndは Strict Mode をオフにしないと動かない問題があり他のライブラリを探していたところ、今回 dnd-kit という良さげなものに出会いました。サイトのデザインもスタイリッシュで期待が高まります。
npm trends を見ると、他の2つにはまだ大きく差をつけられていますが、じわじわと伸びているようです。それにしても、react-dnd と react-beautiful-dnd はすごい拮抗しているんですね。
今回一通り触ってみましたので、どんな感じがご紹介したいと思います。
概要
以下、公式サイトに書かれている内容の要約になります。
Overview(抜粋)
dnd-kit の特徴としては以下のようなものが挙げられます。
-
機能が豊富
- カスタマイズ可能な衝突検知アルゴリズム、複数のアクティベーター、ドラッグ可能オーバーレイ、ドラッグハンドラー等の多くの機能を提供しています
-
React製
- 提供されている2つのhooks、
useDraggable
とuseDroppable
を使えば、DOM のラッパーを作成することなくすぐに DnD が実装できます
- 提供されている2つのhooks、
-
外部依存もなくて軽量
- コアは 10KB ほどに最小化されていて、他の外部ライブラリには依存していません
-
プリセットがある
- ソートが必要な場合は、
@dnd-kit/sortable
を使ってみてと書いてあります
- ソートが必要な場合は、
早速使ってみる
Context
他のライブラリも同じような形ですが、Provider をドラッグやドロップをさせたい要素全体を覆うように設定します。
import React from 'react';
import {DndContext} from '@dnd-kit/core';
import {Draggable} from './Draggable';
import {Droppable} from './Droppable';
function App() {
return (
<DndContext>
<Draggable />
<Droppable />
</DndContext>
)
}
Droppable
ドロップ可能なコンポーネントを作成していきますが、そのためには useDroppable
という hooks を利用します。
useDroppable
に対しては以下の2つが必要です。
- ドロップ可能にしたい領域のDOM要素に対して、
ref
を渡す - 全てのドロップ可能なコンポーネントで一意になるように、ユニークIDを振る
ドラッグ可能な要素が、ドロップ可能な要素の上に来ると、isOver
が true
になります。
import React from 'react';
import {useDroppable} from '@dnd-kit/core';
function Droppable(props) {
const {isOver, setNodeRef} = useDroppable({
id: 'droppable',
});
const style = {
color: isOver ? 'green' : undefined,
};
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
}
Draggable
続いて、ドラッグ可能なコンポーネントを作ります。 useDraggable
という hooks を利用します。
こちらは以下の設定が必要です。
- ドラッグできるように、DOMイベントを検知する
listeners
とattributes
を要素に対して渡す - 全てのドロップ可能なコンポーネントで一意になるように、ユニークIDを振る
アイテムがドラッグされているときtransform
プロパティは座標を返し、中身の構造は以下のようになっています。
{x: number, y: number, scaleX: number, scaleY: number}
import React from 'react';
import {useDraggable} from '@dnd-kit/core';
function Draggable(props) {
const {attributes, listeners, setNodeRef, transform} = useDraggable({
id: 'draggable',
});
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{props.children}
</button>
);
}
ドロップとドラッグを組み合わせる
Droppable
と Draggable
の要素が作成できたら、DnDContext
に onDragEnd
を渡して完了です。
import React, {useState} from 'react';
import {DndContext} from '@dnd-kit/core';
import {Droppable} from './Droppable';
import {Draggable} from './Draggable';
function App() {
const containers = ['A', 'B', 'C'];
const [parent, setParent] = useState(null);
const draggableMarkup = (
<Draggable id="draggable">Drag me</Draggable>
);
function handleDragEnd(event) {
const {over} = event;
// ドロップできる領域にドロップした時、親としてidをセット
// それ以外はnullにする
setParent(over ? over.id : null);
}
return (
<DndContext onDragEnd={handleDragEnd}>
{parent === null ? draggableMarkup : null}
{containers.map((id) => (
// idを受け取ったらDroppableコンポーネントを更新
// useDroppable にpropsを渡す
<Droppable key={id} id={id}>
{parent === id ? draggableMarkup : 'Drop here'}
</Droppable>
))}
</DndContext>
);
};
動くコードはこちら
Integration
tanstack/react-table と合わせて使ってみました!
- @tanstack/react-table
- @dnd-kit/core
- @dnd-kit/modifiers
- @dnd-kit/sortable
- react-icons
Discussion
ちょっとした報告ですが、react-dnd, react-beautiful-dnd のリンク先が逆になってますね
あ、ほんとですね!ありがとうございます 🙇🏻