はじめに
「ドラッグ&ドロップを実装したいけれど、ライブラリのバンドルサイズが気になる」「react-beautiful-dndを使っていたが、メンテナンスが止まってしまった」——そんな悩みを抱えているフロントエンドエンジニアは少なくないはずです。
実は react-beautiful-dnd は開発元のAtlassian自身によってすでにアーカイブ済みとなっており、後継として同社が新たに送り出したのが Pragmatic Drag and Drop です。Trello、Jira、Confluenceといった巨大プロダクトの裏側で実際に使われているライブラリで、「速さ」と「軽さ」を徹底的に追求した設計になっています。今回はこのライブラリの特徴と使い方を、実際のコード例とともに紹介します。
とはいえ、読むより触った方が早いと思います。ドラッグ&ドロップの挙動は文章より実際に手を動かした方が伝わるので、先に触ってみたい方はこちらからどうぞ。
Pragmatic Drag and Dropとは
Pragmatic Drag and Dropは、Atlassianが開発しているフレームワーク非依存のドラッグ&ドロップライブラリです。公式には「Fast drag and drop for any experience on any tech stack」と謳われており、React・Vue・Svelte・Angularなど、どのフレームワークからでも利用できます。
react-beautiful-dndがReact専用かつ独自のレンダリングロジックを内包していたのに対し、Pragmatic Drag and Dropはブラウザ標準のHTML Drag and Drop APIを土台にしたヘッドレス設計を採用しており、スタイリングや描画をアプリ側で自由にコントロールできる点が大きな違いです。
主な特徴
- 圧倒的な軽量さ - コアパッケージは約4.7kBとごく小さく、必要な機能だけをオプションパッケージとして追加していく構成
- フレームワーク非依存 - 内部でReactに依存せず、DOM要素とイベントを直接扱うため、Vue・Svelte・素のJavaScriptでも利用可能
- クロスブラウザ・クロスデバイス対応 - Firefox・Safari・Chromeのデスクトップに加え、iOS・Androidでも動作を保証
- 仮想化リストとの相性の良さ - 大量アイテムを扱う仮想スクロールUIでもパフォーマンスを損なわずに実装できる
- アクセシビリティ対応 - キーボード操作や支援技術向けのツールチェーンが用意されている
インストール
npm・yarn・pnpmのいずれでもインストール可能です。まずはコアパッケージを導入しましょう。
# npm
npm install @atlaskit/pragmatic-drag-and-drop
# yarn
yarn add @atlaskit/pragmatic-drag-and-drop
# pnpm
pnpm add @atlaskit/pragmatic-drag-and-drop
ドロップ位置を示すインジケーターや自動スクロールなど、追加機能が必要な場合はオプションパッケージを別途インストールします。
npm install @atlaskit/pragmatic-drag-and-drop-react-drop-indicator
npm install @atlaskit/pragmatic-drag-and-drop-auto-scroll
Pragmatic Drag and Dropのサンプルを動かす
Pragmatic Drag and Dropの根幹は、draggableでドラッグ元を登録し、dropTargetForElementsでドロップ先を登録し、monitorForElementsでドロップの結果を一元的に受け取るという3ステップです。まずはこの3つを組み合わせた実物を見てみましょう。下のプレイグラウンドではタスクカードを「未着手」と「完了」の間で自由にドラッグできます。
要点だけを抜き出すと、次のようになります。getInitialDataでドラッグ元にデータを持たせ、getDataでドロップ先にもデータを持たせ、monitorForElementsのonDropで両方のデータを突き合わせて状態を更新する、という流れです。
draggable({
element: cardEl,
getInitialData: () => ({ id: 'card-1' }),
})
dropTargetForElements({
element: zoneEl,
getData: () => ({ zone: 'done' }),
})
monitorForElements({
onDrop({ source, location }) {
const zone = location.current.dropTargets[0]?.data.zone
console.log(source.data.id, '→', zone)
},
})
実際に動かせるものが下です。カード「設計レビュー」や「コードレビュー」を「完了」ゾーンまでドラッグして離してみてください。
カードを「完了」ゾーンの上まで運ぶとdropTargetForElementsのonDragEnterが発火して背景色が変わり、離した瞬間にmonitorForElementsのonDrop内でsetCardsが呼ばれてzoneが書き換わるのが確認できるはずです。試しにinitialCardsの3件目をzone: 'todo'に変えると初期表示が変わりますし、Zoneをもう1つ増やしてzoneの種類を3つにすれば、後述するカンバンボードにそのまま拡張できます。
基本的な使い方
Pragmatic Drag and Dropの中核となるのは、draggable(ドラッグ元の設定)、dropTargetForElements(ドロップ先の設定)、monitorForElements(全体の監視)という3つの関数です。
まずは、要素をドラッグ可能にする最小構成です。
import { useEffect, useRef, useState } from 'react';
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
function DraggableCard({ id, label }: { id: string; label: string }) {
const ref = useRef<HTMLDivElement>(null);
const [dragging, setDragging] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
return draggable({
element: el,
getInitialData: () => ({ id }),
onDragStart: () => setDragging(true),
onDrop: () => setDragging(false),
});
}, [id]);
return (
<div ref={ref} style={{ opacity: dragging ? 0.5 : 1 }}>
{label}
</div>
);
}
続いて、ドロップを受け付ける領域を定義します。canDropでドロップ可否の条件を、onDragEnter/onDragLeaveでホバー時の見た目の変化を制御できます。
import { useEffect, useRef, useState } from 'react';
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
function DropZone({ zoneId }: { zoneId: string }) {
const ref = useRef<HTMLDivElement>(null);
const [isOver, setIsOver] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
return dropTargetForElements({
element: el,
getData: () => ({ zoneId }),
onDragEnter: () => setIsOver(true),
onDragLeave: () => setIsOver(false),
onDrop: () => setIsOver(false),
});
}, [zoneId]);
return (
<div
ref={ref}
style={{
backgroundColor: isOver ? 'lightgreen' : 'whitesmoke',
minHeight: 120,
border: '2px dashed #ccc',
}}
>
ここにドロップ
</div>
);
}
最後に、ドロップが実際に発生したタイミングでの状態更新はmonitorForElementsで一元管理します。これはコンポーネントツリーのどこに配置してもよく、アプリ全体の状態管理と組み合わせやすい設計になっています。
import { useEffect } from 'react';
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
function useDropMonitor(onCardMoved: (cardId: string, zoneId: string) => void) {
useEffect(() => {
return monitorForElements({
onDrop({ source, location }) {
const destination = location.current.dropTargets[0];
if (!destination) return;
const cardId = source.data.id as string;
const zoneId = destination.data.zoneId as string;
onCardMoved(cardId, zoneId);
},
});
}, [onCardMoved]);
}
実践的なユースケース
カンバンボードでのカラム間移動
もっとも身近な活用例は、Trelloのようなカンバンボードです。カラムをdropTargetForElementsで定義し、カード1枚1枚をdraggableにすることで、複数カラム間でカードを自由に移動させるUIを構築できます。
function KanbanBoard() {
const [cards, setCards] = useState([
{ id: 'card-1', zoneId: 'todo', label: '設計レビュー' },
{ id: 'card-2', zoneId: 'doing', label: 'API実装' },
]);
useDropMonitor((cardId, zoneId) => {
setCards((prev) =>
prev.map((card) => (card.id === cardId ? { ...card, zoneId } : card)),
);
});
const zones = ['todo', 'doing', 'done'];
return (
<div style={{ display: 'flex', gap: 16 }}>
{zones.map((zoneId) => (
<div key={zoneId} style={{ flex: 1 }}>
<h3>{zoneId}</h3>
<DropZone zoneId={zoneId} />
{cards
.filter((card) => card.zoneId === zoneId)
.map((card) => (
<DraggableCard key={card.id} id={card.id} label={card.label} />
))}
</div>
))}
</div>
);
}
実際に動かせる3カラム版が下です。「未着手」「進行中」「完了」の3ゾーンをdropTargetForElementsで用意し、カードのzoneIdをmonitorForElementsのドロップ結果に応じて書き換えています。
カードを「進行中」や「完了」のカラムへドラッグすると、Column側のisOverがtrueになって背景色が変わり、離した瞬間にKanbanBoard内のsetTasksでzoneIdが更新されます。zones配列にもう1つオブジェクトを足せば、カラム数はそのまま4つ以上にも拡張できます。
リスト内の並び替え(ソート)
カラムをまたぐ移動だけでなく、1つのリスト内で項目の順序を入れ替えたいケースもよくあります。この場合は@atlaskit/pragmatic-drag-and-drop-hitboxパッケージのattachClosestEdge・extractClosestEdgeを使い、ドラッグ中の要素がドロップ先の上半分・下半分のどちらに近いかを判定します。
dropTargetForElements({
element: itemEl,
getData: ({ input, element }) =>
attachClosestEdge({ index }, { element, input, allowedEdges: ['top', 'bottom'] }),
onDrop: ({ self, source }) => {
const edge = extractClosestEdge(self.data) // 'top' | 'bottom'
const next = reorderWithEdge({
list: items,
startIndex: source.data.index,
indexOfTarget: index,
closestEdgeOfTarget: edge,
axis: 'vertical',
})
},
})
実際に動かせるものが下です。タスクをドラッグして、他のタスクの上半分・下半分どちらでドロップするかで挿入位置が変わります。
上半分にドロップするとextractClosestEdgeが'top'を返して対象の直前に、下半分だと'bottom'を返して直後に挿入されるのが分かるはずです。並び替えの実体はreorderWithEdgeが担っており、startIndex(動かした項目)とindexOfTarget(ドロップ先)、closestEdgeOfTarget(上下どちらか)を渡すだけで新しい配列を返してくれます。initialItemsの順序を変えれば初期表示の並びもそのまま変わります。
外部ファイルのドロップ受け付け
Pragmatic Drag and Dropは要素同士のドラッグだけでなく、OSのファイルマネージャーからブラウザへファイルをドラッグする操作にも対応しています。@atlaskit/pragmatic-drag-and-drop/external/adapterのdropTargetForExternalと、@atlaskit/pragmatic-drag-and-drop/external/fileのcontainsFiles・getFilesを組み合わせるのがポイントです。
dropTargetForExternal({
element: dropZoneEl,
canDrop: containsFiles, // OS由来のファイルドラッグだけを受け付ける
onDrop: ({ source }) => {
const files = getFiles({ source })
console.log(files.map((f) => f.name))
},
})
実際に動かせるものが下です。お使いのPCから任意のファイルをドラッグして、枠内にドロップしてみてください。ファイル名とサイズが一覧に追加されます。
枠内にファイルを重ねるとdropTargetForExternalのonDragEnterが発火して枠線内の背景が変わり、離すとonDrop内のgetFiles({ source })がドロップされたFileオブジェクトの配列を返すので、そこからnameとsizeを取り出して一覧に追加しています。canDrop: containsFilesを外せばテキストや画像URLなど別種のドラッグも受け付けられるようになりますが、その場合はgetFilesの代わりにgetTextやgetHTMLなど対応するユーティリティを使う必要があります。
このほかにも、テーブルの行を並べ替えたり、仮想化リストと組み合わせて大量アイテムを扱ったりといった用途にも、ここまで紹介したdraggable・dropTargetForElements・monitorForElementsの組み合わせで対応できます。すべてDOM要素とプレーンなイベントハンドラの組み合わせなので、既存のUIコンポーネントに後付けしやすいのも実務上のメリットです。
まとめ
Pragmatic Drag and Dropは、react-beautiful-dndのようなReact専用ライブラリとは一線を画す、フレームワーク非依存かつ軽量なドラッグ&ドロップの実装基盤です。draggable・dropTargetForElements・monitorForElementsという3つのシンプルな関数を組み合わせるだけで、カンバンボードのような複雑なUIも段階的に構築できることがわかりました。
react-beautiful-dndからの移行を検討している方には公式の移行用パッケージも用意されているので、まずは小さなドラッグ&ドロップUIから触ってみて、その軽さとブラウザ間の安定動作を体感してみてはいかがでしょうか。
