はじめに
ブラウザのネイティブなDrag and Drop API(HTML5 Drag and Drop API)を素で触ったことがある方なら分かると思いますが、dragstartやdragover、dropといったイベントを自分で組み合わせて状態管理をするのは、想像以上に骨が折れます。e.preventDefault()を書き忘れるだけでドロップが効かなくなったり、どの要素の上にいるかの判定を自前で持ち回ったりと、Reactの宣言的な書き方とは相性が悪い領域です。
React-DnDは、このドラッグ&ドロップの状態管理をReactのHooksに落とし込んだライブラリです。useDragとuseDropという2つのHooksだけで、ドラッグ中かどうか・ドロップ可能かどうかといった状態をコンポーネントのpropsのように扱えるようになります。
とはいえ、読むより触った方が早いと思います。カードをつまんで下の枠にドロップする最小構成を用意したので、先に挙動を見たい方はこちらからどうぞ。
React-DnDとは
React-DnDは、ReactコンポーネントにDrag and Dropの振る舞いを付与するためのライブラリです。HTML5 Drag and Drop APIをラップしたHTML5Backendのほか、タッチデバイス向けのTouchBackend、テスト用のTestBackendなど、実行環境に応じてバックエンドを差し替えられる設計になっています。
主な特徴
- Hooksベースの宣言的API -
useDragとuseDropで、ドラッグ元・ドロップ先の振る舞いをコンポーネント内に閉じたロジックとして書ける - 監視状態(Monitor)の取得 -
isDraggingやisOver、canDropといった状態をcollect関数経由でリアルタイムに取得できる - バックエンドの差し替え -
HTML5BackendとTouchBackendを切り替えるだけで、PCとタッチデバイス両方に対応できる - ロジックと見た目の分離 - ドラッグ&ドロップの状態管理とレンダリングが分かれているため、見た目だけを自由にカスタマイズしやすい
インストール
React-DnD本体と、実行環境に応じたバックエンドの両方をインストールします。PCブラウザ向けであればreact-dnd-html5-backendを使うのが基本です。
npm install react-dnd react-dnd-html5-backend
React-DnDのサンプルを動かす
以下は、useDragでドラッグ可能なカードを作り、useDropでドロップ先の枠を作るサンプルです。ドロップ先にはmonitor.isOver()の状態を渡し、カードが重なっている間だけ背景色を変えています。カードをドロップ先まで運んでみてください。
function Card() {
const [{ isDragging }, drag] = useDrag(() => ({
type: 'CARD',
item: { text: 'つまんでドロップ' },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}));
return <div ref={drag} style={{ opacity: isDragging ? 0.4 : 1 }}>ドラッグ元</div>;
}
function DropZone({ onDrop }) {
const [{ isOver }, drop] = useDrop(() => ({
accept: 'CARD',
drop: onDrop,
collect: (monitor) => ({ isOver: monitor.isOver() }),
}));
return <div ref={drop}>{isOver ? '離すと受け取ります' : 'ここにドロップ'}</div>;
}
実際に動かせるものが下です。useDragが返すdrag関数をDOM要素のrefに渡すことでその要素がドラッグ可能になり、useDropが返すdrop関数を渡すことでその要素がドロップ先になります。
ドラッグ中はカードのopacityが下がり、枠の上に重なるとisOverがtrueになって背景が緑に変わります。accept: 'CARD'の文字列を変えると、そのタイプのアイテムしか受け付けなくなるので、複数種類のドラッグ元とドロップ先を作り分ける際の目印になります。この先の「基本的な使い方」「実践的なユースケース」では、並べ替えやKanbanボードといった実践的なパターンを見ていきます。
基本的な使い方
React-DnDを使うには、まずアプリ全体をDndProviderで囲み、使用するbackendを指定します。PC向けであればHTML5Backendを渡すのが基本です。
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
function App() {
return (
<DndProvider backend={HTML5Backend}>
{/* ドラッグ&ドロップを使うコンポーネント群 */}
</DndProvider>
);
}
ドラッグ元はuseDragで定義します。typeはドロップ先のacceptと対応させる識別子、itemはドロップ時にドロップ先へ渡されるデータです。
import { useDrag } from 'react-dnd';
function Card({ id, text }) {
const [{ isDragging }, drag] = useDrag(() => ({
type: 'CARD',
item: { id, text },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}));
return (
<div ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }}>
{text}
</div>
);
}
ドロップ先はuseDropで定義します。acceptにドラッグ元のtypeと同じ文字列を指定し、dropコールバックでドロップされたときの処理を書きます。
import { useDrop } from 'react-dnd';
function DropTarget({ onDropCard }) {
const [{ isOver }, drop] = useDrop(() => ({
accept: 'CARD',
drop: (item) => onDropCard(item.id),
collect: (monitor) => ({ isOver: monitor.isOver() }),
}));
return <div ref={drop}>{isOver ? '離すと受け取ります' : 'ここにドロップ'}</div>;
}
実践的なユースケース
並べ替え可能なリスト
タスクリストや画像の順序を、ドラッグで直感的に並べ替えたい場面は多くあります。React-DnDでは、リストの各行にuseDragとuseDropを両方適用し、useDropのhoverコールバック内で配列の順序を入れ替えることでこれを実現します。
function Row({ id, index, text, moveRow }) {
const ref = useRef(null);
const [, drop] = useDrop({
accept: 'ROW',
hover(item) {
if (item.index === index) return;
moveRow(item.index, index);
item.index = index;
},
});
const [{ isDragging }, drag] = useDrag({
type: 'ROW',
item: { id, index },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
});
drag(drop(ref));
return <li ref={ref} style={{ opacity: isDragging ? 0.3 : 1 }}>{text}</li>;
}
各行が自分自身をuseDragのドラッグ元にしつつ、同時にuseDropのドロップ先にもなっているのがポイントです。他の行の上に重なった瞬間(hover)にmoveRowを呼んで配列を並べ替えるため、指を離す前にリアルタイムで順序が入れ替わるのが確認できます。
複数リスト間でのアイテム移動(Kanbanボード)
タスク管理ツールのように、複数のリスト間でアイテムを移動させたい場合は、ドロップ先ごとにacceptするtypeを揃えつつ、dropコールバックの中でどのリストに移動したかを判定します。
function Column({ status, tasks, moveTask }) {
const [{ isOver }, drop] = useDrop(() => ({
accept: 'TASK',
drop: (item) => moveTask(item.id, status),
collect: (monitor) => ({ isOver: monitor.isOver() }),
}));
return (
<div ref={drop} style={{ background: isOver ? '#eef' : '#f5f5f5' }}>
{tasks.map((t) => <TaskCard key={t.id} {...t} />)}
</div>
);
}
3つのColumnはいずれも同じaccept: 'TASK'を指定しているため、どの列にもドロップできます。dropコールバックで受け取ったstatusをタスクの状態として保存し直しているだけなので、カードを列間で動かすとすぐに再配置されるのが分かります。列を増やしたい場合は、statusの種類とColumnを追加するだけで対応できます。
カスタムドラッグレイヤー
デフォルトのドラッグ中の見た目(ブラウザのネイティブなゴースト画像)を変えたい場合は、useDragLayerを使ってドラッグ中の要素を自前で描画します。react-dnd-html5-backendが提供するgetEmptyImage()をドラッグプレビューに設定してネイティブのゴーストを消し、代わりにカーソル位置に追従する要素を表示します。
function CustomDragLayer() {
const { item, isDragging, currentOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
isDragging: monitor.isDragging(),
currentOffset: monitor.getSourceClientOffset(),
}));
if (!isDragging || !currentOffset) return null;
return (
<div style={{
position: 'fixed', left: 0, top: 0, pointerEvents: 'none',
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px) rotate(-12deg)`,
}}>
{item.emoji}
</div>
);
}
useDragが返す3つ目の要素(preview関数)にgetEmptyImage()を渡すことで、ブラウザ標準の半透明なゴースト画像を消しています。そのうえでuseDragLayerから取得したcurrentOffset(カーソル位置)を使って、回転や拡大を加えた独自のプレビューをposition: fixedで描画しているのが仕組みです。rotate(-12deg)の角度やscale(1.4)の倍率を変えると、プレビューの見た目を自由に調整できます。
まとめ
React-DnDは、ブラウザのDrag and Drop APIが持つ複雑なイベント管理を、useDragとuseDropという2つのHooksに落とし込んだライブラリです。ドラッグ元とドロップ先のロジックがコンポーネント内に閉じるため、並べ替えリストやKanbanボードのような複雑なUIも、状態管理をReactの流儀に乗せたまま組み立てられます。useDragLayerまで使えば、見た目のカスタマイズも自由度高く行えます。
まずは1つのドラッグ元とドロップ先をuseDrag・useDropで組んでみて、React-DnDの状態がどうpropsのように扱えるかを体感してみてください。
