はい、承知いたしました。Reactでドラッグアンドドロップを実装するためのライブラリ react-beautiful-dnd について、詳細な説明を含む記事を作成します。約5000語を目指し、導入から詳細な使い方、応用、注意点までを網羅します。
Reactでドラッグアンドドロップ実装!react-beautiful-dnd 入門
Webアプリケーションにおけるユーザーエクスペリエンスの向上において、ドラッグアンドドロップ(D&D)機能は非常に強力なツールです。ファイルのアップロード、タスクリストの並び替え、ダッシュボードのカスタマイズなど、様々な場面で直感的で効率的な操作を提供します。
しかし、Reactのような宣言的なUIライブラリでDOM操作が伴うドラッグアンドドロップをゼロから実装するのは容易ではありません。DOMイベントのハンドリング、要素の位置計算、ステート管理、パフォーマンス、そしてアクセシビリティといった多くの課題を解決する必要があります。
そこで登場するのが、react-beautiful-dnd です。これは、Atlassian(JiraやTrelloの開発元)によって開発されたReactのためのドラッグアンドドロップライブラリであり、「美しく、アクセスしやすく、そしてパフォーマンスが高い」リストやテーブルのような垂直方向のリストに最適化されています。使いやすさ、洗練されたアニメーション、そして組み込みのアクセシビリティサポートが特徴です。
この記事では、react-beautiful-dnd を使ってReactアプリケーションにドラッグアンドドロップ機能を実装する方法を、基本から応用まで徹底的に解説します。約5000語というボリュームで、概念の理解から具体的なコード実装、さらにはパフォーマンスやアクセシビリティに関する考慮事項まで、この記事を読めば react-beautiful-dnd を使いこなすための知識が網羅できるように構成しました。
対象読者:
- Reactでのドラッグアンドドロップ実装に興味がある方
react-beautiful-dndを使ってみたいが、どこから始めれば良いか分からない方- 既存の
react-beautiful-dndの実装をもっと深く理解したい方 - Reactアプリケーションのユーザーエクスペリエンスを向上させたい方
さあ、react-beautiful-dnd の世界に飛び込みましょう!
1. react-beautiful-dnd の基本概念
react-beautiful-dnd を理解する上で最も重要なのは、その核となる3つのコンポーネントです。これらのコンポーネントを組み合わせることで、ドラッグ可能なアイテムとドロップ可能な領域を定義し、ドラッグアンドドロップのイベントを管理します。
DragDropContext: ドラッグアンドドロップ機能を提供する範囲を定義する最も外側のコンポーネントです。アプリケーション全体、あるいは特定のセクションをこのコンポーネントでラップします。ドラッグイベントが発生した際に呼び出されるコールバック関数(onDragEndなど)を設定します。Droppable: アイテムをドロップできる領域(ドロップゾーン)を定義します。リストコンテナ、ボードの列などがこれに該当します。Droppableの子要素は、ドラッグ可能なアイテム(Draggable)を配置するための領域となります。Droppableはレンダリングプロップ(レンダー関数)パターンを使用し、子関数にprovidedとsnapshotオブジェクトを渡します。Draggable: ドラッグ可能な個々のアイテムを定義します。リストの各項目、ボードの各カードなどがこれに該当します。Draggableもレンダリングプロップパターンを使用し、子関数にprovidedとsnapshotオブジェクトを渡します。providedオブジェクトには、ドラッグ機能を有効にするためにDOM要素に適用する必要があるプロパティが含まれています。
これらのコンポーネントは、以下のような階層構造で使用するのが典型的です。
jsx
<DragDropContext onDragEnd={handleDragEnd}>
{/* 複数の Droppable を配置可能 */}
<Droppable droppableId="list-1">
{(provided, snapshot) => (
<div
{...provided.droppableProps} // Droppable 領域に適用するプロパティ
ref={provided.innerRef} // 領域の DOM 要素への参照
style={{ /* snapshot.isDraggingOver などに応じたスタイル */ }}
>
{/* 複数の Draggable を配置可能 */}
<Draggable draggableId="item-1" index={0}>
{(provided, snapshot) => (
<div
{...provided.draggableProps} // Draggable アイテムに適用するプロパティ
{...provided.dragHandleProps} // ドラッグハンドルに適用するプロパティ (通常は draggableProps と同じ要素に適用)
ref={provided.innerRef} // アイテムの DOM 要素への参照
style={{ /* snapshot.isDragging などに応じたスタイル */ }}
>
{/* アイテムのコンテンツ */}
Item Content
</div>
)}
</Draggable>
{/* 他の Draggable アイテム... */}
{provided.placeholder} {/* ドラッグ中のアイテムの元の位置を確保するプレースホルダー */}
</div>
)}
</Droppable>
{/* 他の Droppable 領域... */}
</DragDropContext>
この構造を理解することが、react-beautiful-dnd を使い始める上での第一歩となります。特に provided オブジェクトと snapshot オブジェクトの役割、そして innerRef、droppableProps、draggableProps、dragHandleProps を正しくDOM要素に適用することが重要です。
2. 環境構築とインストール
react-beautiful-dnd を使うための環境を準備しましょう。
前提条件:
- Node.js と npm または yarn がインストールされていること。
- Reactの基本的な知識があること。
ステップ:
-
新しいReactプロジェクトを作成(または既存プロジェクトを使用)
Create React App または Vite を使うのが簡単です。- Create React App の場合:
bash
npx create-react-app my-dnd-app
cd my-dnd-app - Vite の場合:
bash
npm create vite@latest my-dnd-app --template react
cd my-dnd-app
npm install # または yarn install
- Create React App の場合:
-
react-beautiful-dndのインストール
プロジェクトのルートディレクトリで以下のコマンドを実行します。“`bash
npm install react-beautiful-dndまたは yarn add react-beautiful-dnd
“`
これで準備は完了です。Reactコンポーネント内で react-beautiful-dnd のコンポーネントを使用できるようになります。
3. 最小限のドラッグアンドドロップリスト実装
最も基本的な例として、一つのリスト内でアイテムを並び替えるドラッグアンドドロップ機能を実装してみましょう。
まず、リストに表示する簡単なデータを用意します。
“`javascript
// src/initial-data.js
const initialData = [
{ id: ‘item-1’, content: ‘項目 1’ },
{ id: ‘item-2’, content: ‘項目 2’ },
{ id: ‘item-3’, content: ‘項目 3’ },
{ id: ‘item-4’, content: ‘項目 4’ },
{ id: ‘item-5’, content: ‘項目 5’ },
];
export default initialData;
“`
次に、このデータを使ってドラッグアンドドロップ可能なリストコンポーネントを作成します。
“`jsx
// src/App.js または src/components/TaskList.js
import React, { useState } from ‘react’;
import { DragDropContext, Droppable, Draggable } from ‘react-beautiful-dnd’;
import initialData from ‘./initial-data’;
import ‘./App.css’; // 後でスタイルを追加する場合
// 配列の要素を並び替えるユーティリティ関数
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
function App() {
const [items, setItems] = useState(initialData);
// ドラッグ終了時の処理
const onDragEnd = (result) => {
// ドロップ先がない場合(範囲外にドロップ)
if (!result.destination) {
return;
}
// 同じ位置にドロップされた場合
if (
result.source.droppableId === result.destination.droppableId &&
result.source.index === result.destination.index
) {
return;
}
// リストの並び替え
const newItems = reorder(
items,
result.source.index,
result.destination.index
);
setItems(newItems);
};
return (
My Draggable List
{(provided, snapshot) => (
{items.map((item, index) => (
{(provided, snapshot) => (
{item.content}
)}
))}
{provided.placeholder} {/ ここが重要! /}
)}
);
}
export default App;
“`
コードの説明:
initialData: ドラッグアンドドロップするアイテムの初期データです。各アイテムには一意のidと表示するcontentが必要です。reorder関数: これは配列内の要素を並び替えるためのヘルパー関数です。spliceメソッドを使って、指定された開始インデックスの要素を削除し、終了インデックスに挿入しています。これはonDragEndで使用されます。useState(initialData): リストの現在の状態を管理するためにReactのuseStateフックを使用しています。onDragEnd関数:DragDropContextの必須プロップです。ドラッグ操作が終了したときに呼び出されます。引数resultには、ドラッグ操作に関する詳細情報(ドラッグされたアイテムのID、元の位置、ドロップ先の位置など)が含まれています。!result.destination: アイテムがドロップ可能な領域外にドロップされた場合は何もせずに関数を終了します。result.source.index === result.destination.index: アイテムが元の位置と同じ場所にドロップされた場合も何もせずに関数を終了します。reorder(...): ドロップ先が有効な場合、reorder関数を使ってitems配列の要素を並び替えます。setItems(newItems): 並び替えられた新しい配列でステートを更新します。Reactはステートの変更を検知してUIを再レンダリングし、リストの表示が新しい順序に更新されます。
DragDropContext onDragEnd={onDragEnd}: ドラッグアンドドロップ機能の最上位コンテナです。onDragEndプロップに関数ハンドラーを渡します。Droppable droppableId="my-list": アイテムをドロップできる領域を定義します。droppableIdはそのDroppableを一意に識別するための文字列です。- レンダリングプロップ
(provided, snapshot) => (...)を使用します。 {...provided.droppableProps}:DroppableのDOM要素(ここではdiv)に適用する必要があるプロパティです。データ属性などが含まれます。ref={provided.innerRef}:react-beautiful-dndがDOMノードを測定するために必要です。Droppableの最も外側のDOM要素に適用します。snapshot.isDraggingOver: このDroppable領域の上にドラッグ中のアイテムがあるかどうかを示すブール値です。これを使ってスタイルを動的に変更できます(例: 背景色)。{provided.placeholder}: 非常に重要です。ドラッグ中のアイテムが元の位置から取り除かれたときに、そのアイテムが占めていた空間を確保するために必要です。これはDroppableの子要素リストの最後にレンダリングする必要があります。
- レンダリングプロップ
Draggable key={item.id} draggableId={item.id} index={index}: ドラッグ可能な個々のアイテムを定義します。keyプロップはReactのリストレンダリングの要件です。draggableIdはそのDraggableを一意に識別するための文字列です。これはアイテムのidと同じにするのが一般的で、必須です。indexはそのDraggableが親Droppableの子要素リスト内で現在何番目にあるかを示すインデックスです。これも必須です。- レンダリングプロップ
(provided, snapshot) => (...)を使用します。 ref={provided.innerRef}:react-beautiful-dndがDOMノードを測定するために必要です。Draggableの最も外側のDOM要素に適用します。{...provided.draggableProps}:DraggableのDOM要素に適用する必要があるプロパティです。ドラッグ機能を有効にするための属性などが含まれます。{...provided.dragHandleProps}: どの要素をドラッグハンドル(実際にドラッグを開始するためにクリック&ドラッグする部分)にするかを指定します。 ここではdraggablePropsと同じdivに適用しているため、アイテム全体がドラッグ可能になります。特定の要素だけをドラッグハンドルにしたい場合は、その特定の要素にのみこのプロパティを適用します(後述)。snapshot.isDragging: このアイテムが現在ドラッグ中かどうかを示すブール値です。これを使ってスタイルを動的に変更できます(例: 背景色や影)。...provided.draggableProps.style:react-beautiful-dndはドラッグ中にインラインスタイルを適用します。カスタムスタイルを定義する際は、provided.draggableProps.styleをスプレッドして既存のスタイルを上書きしないように含めることが推奨されます。
このコードを実装すれば、基本的なドラッグアンドドロップ可能なリストが完成します。ブラウザで実行し、リストのアイテムをドラッグして並び替えができることを確認してみてください。
4. react-beautiful-dnd の詳細な機能
基本を押さえたところで、各コンポーネントのより詳細なプロップや利用方法を見ていきましょう。
4.1. DragDropContext
DragDropContext はドラッグアンドドロップ機能のライフサイクルを管理します。主なプロップは以下の通りです。
onDragStart?(start: DragStart): void: ドラッグが開始された直後に呼び出されます。引数startには、ドラッグされたアイテムの情報(ID, ソースのdroppableIdとindex)が含まれます。UIにドラッグ開始を示すフィードバック(例: スクロール無効化、他のアイテムのスタイル変更)を加えたい場合に使用します。onDragUpdate?(update: DragUpdate): void: ドラッグ中にアイテムが移動したり、他のDroppable領域に入ったりするたびに呼び出されます。頻繁に呼び出されるため、重い処理は避けるべきです。引数updateには、現在のドラッグ状態に関する詳細(startの情報に加えて、現在の位置、ホバーしているDroppableなど)が含まれます。ドラッグ中のリアルタイムなUIフィードバックに使用できます。onDragEnd(result: DropResult): void: 必須プロップ。ドラッグ操作が終了し、アイテムがドロップされたときに呼び出されます。引数resultには、ドラッグ操作の最終結果(元の位置source、ドロップ先の位置destination、ドラッグされたアイテムのIDdraggableId、アイテムのタイプtypeなど)が含まれます。この関数内でアプリケーションのステート(データの並び順)を更新する処理を行います。canCaptureTrailingSpace?: boolean: ドラッグ中の要素の後ろに余分なスペースをキャプチャするかどうか。特定のレイアウトで役立つ場合があります。デフォルトはfalse。enableDefaultSensors?: boolean: デフォルトの入力センサー(マウス、キーボード、タッチ)を有効にするかどうか。通常はtrueのまま使用します。カスタムセンサーを使用する場合にfalseにします。
onDragEnd の result: DropResult オブジェクトの詳細:
result オブジェクトは以下のプロパティを持ちます。
draggableId: string: ドラッグされたDraggableのID。type: string: ドラッグされたDraggableのタイプ(後述)。source: DraggableLocation: ドラッグされたアイテムの元の位置。droppableId: string: 元のDroppableのID。index: number: 元のDroppable内でのインデックス。
destination: ?DraggableLocation: アイテムがドロップされた位置。ドロップ可能な領域外にドロップされた場合はnullになります。droppableId: string: ドロップ先のDroppableのID。index: number: ドロップ先のDroppable内でのインデックス。
mode: 'FLUID' | 'SNAP': ドラッグ中の動作モード(FLUIDがデフォルトで滑らかなアニメーション、SNAPはスナップするような動き)。combine?: Combine: ドロップが別のDraggableと結合された場合の情報(例: サブタスク化)。高度な機能です。
onDragEnd で最もよく使うのは result.source と result.destination です。これらの情報を使って、どのリストの何番目のアイテムが、どのリストの何番目に移動したかを判断し、ステートを更新します。
4.2. Droppable
Droppable はアイテムをドロップできる領域を定義します。主なプロップは以下の通りです。
droppableId: string: 必須プロップ。DragDropContext内で一意である必要があります。onDragEndのresultオブジェクトでこのIDを使ってどのDroppable内でイベントが発生したかを識別します。type?: string: このDroppableが受け入れられるDraggableのタイプを指定します。後述の「複数のリスト間での移動」で重要になります。省略した場合のデフォルトは"DEFAULT"です。isDropDisabled?: boolean: このDroppableへのドロップを一時的に無効にするかどうかを指定します。direction?: 'vertical' | 'horizontal': このDroppable内のアイテムが垂直方向(デフォルト)に並ぶか、水平方向に並ぶかを指定します。ignoreContainerClipping?: boolean: ドラッグ中にコンテナのクリッピングを無視するかどうか。オーバーフローが非表示になっているコンテナで役立つ場合があります。render?: レンダリングプロップ関数(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => React.Nodeを使用します。
DroppableProvided オブジェクトの詳細:
Droppable のレンダリングプロップに渡される provided オブジェクトは以下のプロパティを持ちます。
innerRef: ?(HTMLElement | null) => void: 必須。Droppableの最も外側のDOM要素への参照を設定するために使用します。ref={provided.innerRef}のように要素に適用します。droppableProps: DroppableProps: 必須。DroppableのDOM要素に適用する必要があるDOMプロパティ(データ属性など)です。{...provided.droppableProps}のように要素に適用します。placeholder: ?React.Node: 必須。ドラッグ中のアイテムが占有していたスペースを確保するためのプレースホルダー要素です。Droppableの子要素リストの最後にレンダリングする必要があります。draggingOverWith?: ?DraggableId: 現在このDroppableの上にドラッグされているDraggableのIDです。connectWith?: ?DroppableId[]: 現在このDroppableに接続されている他のDroppableのIDの配列です。
DroppableStateSnapshot オブジェクトの詳細:
Droppable のレンダリングプロップに渡される snapshot オブジェクトは、現在のドラッグ状態に関する情報を提供し、UIを動的に変更するために使用します。
isDraggingOver: boolean: いずれかのDraggableがこのDroppableの上に現在ドラッグされているかどうか。draggingOverWith?: ?DraggableId: 上記provided.draggingOverWithと同じです。isUsingPlaceholder: boolean: プレースホルダーが使用されているかどうか。
これらのスナップショットプロパティを使って、例えば isDraggingOver が true のときに Droppable の背景色を変えるなどのスタイリングを行うことができます。
4.3. Draggable
Draggable はドラッグ可能な個々のアイテムを定義します。主なプロップは以下の通りです。
draggableId: string: 必須プロップ。DragDropContext内(より正確には同じtypeのDroppable内)で一意である必要があります。onDragEndのresultオブジェクトでこのIDを使ってどのアイテムがドラッグされたかを識別します。index: number: 必須プロップ。親Droppableの子要素リスト内でのこのDraggableの現在のインデックスです。データ配列のインデックスと一致している必要があります。isDragDisabled?: boolean: このDraggableのドラッグを一時的に無効にするかどうかを指定します。disableInteractiveElementBlocking?: boolean:Draggable内のボタンやリンクなどのインタラクティブ要素のデフォルトのドラッグ開始ブロックを無効にするかどうか。shouldRespectForceTouch?: boolean: Force Touch 対応デバイスで Force Touch がドラッグ開始を遅延させるかどうか。render?: レンダリングプロップ関数(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => React.Nodeを使用します。
DraggableProvided オブジェクトの詳細:
Draggable のレンダリングプロップに渡される provided オブジェクトは以下のプロパティを持ちます。
innerRef: ?(HTMLElement | null) => void: 必須。Draggableの最も外側のDOM要素への参照を設定するために使用します。ref={provided.innerRef}のように要素に適用します。draggableProps: DraggableProps: 必須。DraggableのDOM要素に適用する必要があるDOMプロパティ(ドラッグジェスチャーのリスナーなど)です。{...provided.draggableProps}のように要素に適用します。dragHandleProps: ?DragHandleProps: 必須。このプロパティを適用した要素がドラッグハンドルになります。Draggable全体をドラッグ可能にする場合は、draggablePropsと同じ要素に適用します。特定の要素(例: グリップアイコン)に適用することで、その要素のみがドラッグハンドルとなります。placeholder?: ?React.Node: ドラッグ中に元の場所に一時的に表示されるプレースホルダー要素。通常はDroppableレベルのプレースホルダー (provided.placeholder) で十分ですが、Draggableごとにカスタマイズしたい場合に利用できます。
DraggableStateSnapshot オブジェクトの詳細:
Draggable のレンダリングプロップに渡される snapshot オブジェクトは、現在のドラッグ状態に関する情報を提供し、UIを動的に変更するために使用します。
isDragging: boolean: このアイテムが現在ドラッグされているかどうか。draggingOver?: ?DroppableId: このアイテムが現在ドラッグされているDroppableのIDです。combineTargetFor?: ?DraggableId: このアイテムが別のアイテムの結合対象になっている場合、その結合しようとしているアイテムのIDです。mode: 'FLUID' | 'SNAP': 現在のドラッグモードです。
これらのスナップショットプロパティを使って、例えば isDragging が true のときに Draggable の背景色や影を変えるなどのスタイリングを行うことができます。
5. 複雑なシナリオの実装
基本的なリスト内並び替えができるようになったら、より実践的なシナリオに進みましょう。
5.1. 複数のリスト間での移動
カンバンボードのように、複数のリスト(例: Todo, Doing, Done)の間でアイテムを移動できるようにします。
データ構造を複数のリストを持つオブジェクトに変更します。
“`javascript
// src/initial-data-multi.js
const initialData = {
tasks: { // 個々のタスク(Draggableアイテム)
‘task-1’: { id: ‘task-1’, content: ‘資料作成’ },
‘task-2’: { id: ‘task-2’, content: ‘会議準備’ },
‘task-3’: { id: ‘task-3’, content: ‘メール返信’ },
‘task-4’: { id: ‘task-4’, content: ‘コードレビュー’ },
},
columns: { // リスト(Droppable領域)
‘column-1’: {
id: ‘column-1’,
title: ‘Todo’,
taskIds: [‘task-1’, ‘task-2’], // このリストに含まれるタスクのIDの配列
},
‘column-2’: {
id: ‘column-2’,
title: ‘Doing’,
taskIds: [‘task-3’],
},
‘column-3’: {
id: ‘column-3’,
title: ‘Done’,
taskIds: [‘task-4’],
},
},
// リストの表示順序(オプション)
columnOrder: [‘column-1’, ‘column-2’, ‘column-3’],
};
export default initialData;
“`
コンポーネント側では、この新しいデータ構造に合わせてステートを管理し、onDragEnd でリスト間移動のロジックを実装します。
“`jsx
// src/App.js (複数のリスト対応)
import React, { useState } from ‘react’;
import { DragDropContext, Droppable, Draggable } from ‘react-beautiful-dnd’;
import initialData from ‘./initial-data-multi’;
import Column from ‘./components/Column’; // 後で作るカラムコンポーネント
// Helper function for reordering array within the same column
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
// Helper function for moving an item between columns
const move = (source, destination, droppableSource, droppableDestination) => {
const sourceClone = Array.from(source);
const destClone = Array.from(destination);
const [removed] = sourceClone.splice(droppableSource.index, 1);
destClone.splice(droppableDestination.index, 0, removed);
const result = {};
result[droppableSource.droppableId] = sourceClone;
result[droppableDestination.droppableId] = destClone;
return result;
};
function App() {
const [boardData, setBoardData] = useState(initialData);
const onDragEnd = (result) => {
const { destination, source, draggableId } = result;
// ドロップ先がない
if (!destination) {
return;
}
// 同じ場所へドロップ
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const startColumn = boardData.columns[source.droppableId];
const endColumn = boardData.columns[destination.droppableId];
// 1. 同じカラム内での並び替え
if (startColumn.id === endColumn.id) {
const newTaskIds = reorder(
startColumn.taskIds,
source.index,
destination.index
);
const newColumn = {
...startColumn,
taskIds: newTaskIds,
};
const newBoardData = {
...boardData,
columns: {
...boardData.columns,
[newColumn.id]: newColumn,
},
};
setBoardData(newBoardData);
return; // 処理完了
}
// 2. 異なるカラム間での移動
const resultMove = move(
startColumn.taskIds,
endColumn.taskIds,
source,
destination
);
const newStartColumn = {
...startColumn,
taskIds: resultMove[startColumn.id],
};
const newEndColumn = {
...endColumn,
taskIds: resultMove[endColumn.id],
};
const newBoardData = {
...boardData,
columns: {
...boardData.columns,
[newStartColumn.id]: newStartColumn,
[newEndColumn.id]: newEndColumn,
},
};
setBoardData(newBoardData);
};
return (
カンバン風ボード
{/ リスト(カラム)の表示順序でマップ /}
{boardData.columnOrder.map((columnId) => {
const column = boardData.columns[columnId];
const tasks = column.taskIds.map(taskId => boardData.tasks[taskId]);
// 各カラムを Droppable としてレンダリング
return (
<Column
key={column.id}
column={column}
tasks={tasks}
// Column コンポーネント内で Droppable と Draggable をラップ
/>
);
})}
</DragDropContext>
</div>
);
}
export default App;
“`
Column.js コンポーネントを作成します。これは一つの Droppable 領域(カラム)と、その中の複数の Draggable アイテム(タスク)をレンダリングします。
“`jsx
// src/components/Column.js
import React from ‘react’;
import { Droppable, Draggable } from ‘react-beautiful-dnd’;
import Task from ‘./Task’; // 後で作るタスクコンポーネント
function Column(props) {
return (
{props.column.title}
{(provided, snapshot) => (
{props.tasks.map((task, index) => (
))}
{provided.placeholder}
)}
);
}
export default Column;
“`
Task.js コンポーネントを作成します。これは一つの Draggable アイテム(タスクカード)をレンダリングします。
“`jsx
// src/components/Task.js
import React from ‘react’;
import { Draggable } from ‘react-beautiful-dnd’;
function Task(props) {
return (
{(provided, snapshot) => (
{props.task.content}
)}
);
}
export default Task;
“`
複数のリスト間移動のロジック (onDragEnd) の説明:
startColumnとendColumnを、ドラッグ元 (source.droppableId) とドラッグ先 (destination.droppableId) のIDを使ってboardData.columnsから取得します。- 同じカラム内での並び替え:
startColumn.id === endColumn.idの場合に実行されます。reorder関数を使用して、そのカラムのtaskIds配列内の要素を並び替えます。- 並び替えられた
newTaskIdsを含む新しいnewColumnオブジェクトを作成します。 boardDataオブジェクトをスプレッドし、columnsプロパティ内の該当するカラムを新しいnewColumnで更新したnewBoardDataを作成します。setBoardDataでステートを更新します。
- 異なるカラム間での移動:
startColumn.id !== endColumn.idの場合に実行されます。move関数を使用します。この関数は、ソースカラムのtaskIdsからアイテムを削除し、デスティネーションカラムのtaskIdsにアイテムを追加します。結果として、ソースとデスティネーション両方の新しいtaskIds配列を含むオブジェクトを返します。resultMoveオブジェクトを使って、新しいnewStartColumnとnewEndColumnオブジェクトを作成します。boardDataオブジェクトをスプレッドし、columnsプロパティ内のソースカラムとデスティネーションカラムを新しいオブジェクトで更新したnewBoardDataを作成します。setBoardDataでステートを更新します。
この実装により、タスクカードをTodo、Doing、Doneのカラム間で自由にドラッグアンドドロップできるようになります。
5.2. 水平方向のリスト
Droppable の direction プロップを "horizontal" に設定することで、水平方向のリストやグリッドのようなレイアウトに対応できます。
jsx
<Droppable droppableId="horizontal-list" direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={{
backgroundColor: snapshot.isDraggingOver ? 'lightblue' : 'lightgrey',
display: 'flex', // flexboxを使ってアイテムを水平に配置
padding: 8,
overflowX: 'auto', // 横方向のスクロールを可能に
}}
>
{/* Draggable items here */}
{provided.placeholder}
</div>
)}
</Droppable>
水平リストを実装する際は、CSSの display: flex; や display: grid; を利用して子要素(Draggable アイテム)を横方向に並べる必要があります。また、コンテナに overflow-x: auto; を設定して、アイテムが多い場合に横スクロールできるようにすることが一般的です。
5.3. ドラッグハンドルの指定
デフォルトでは Draggable のレンダリングプロップで provided.draggableProps と provided.dragHandleProps を同じ要素に適用することで、アイテム全体がドラッグ可能になります。特定の要素(例: グリップアイコンやヘッダー部分)のみをドラッグハンドルにしたい場合は、その特定の要素にのみ provided.dragHandleProps を適用します。
jsx
<Draggable draggableId={task.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps} // Draggable の外側の要素には必須
style={{
// ... (アイテム全体のスタイル)
}}
>
{/* ドラッグハンドルとして機能する要素 */}
<div
{...provided.dragHandleProps} // dragHandleProps を特定の要素に適用
style={{
width: 30,
height: 30,
backgroundColor: 'grey',
marginRight: 8,
display: 'inline-block',
// ... (ハンドルのスタイル)
}}
>
{/* グリップアイコンなど */}
</div>
{/* アイテムの残りのコンテンツ */}
{task.content}
</div>
)}
</Draggable>
この場合、draggableProps は Draggable のルート要素(この例では一番外側の div)に適用しますが、実際にドラッグを開始するには dragHandleProps が適用された内部の div をクリック&ドラッグする必要があります。
5.4. カスタムスタイリング
react-beautiful-dnd はドラッグ中やドロップオーバー時などに自動でスタイルを適用しますが、スナップショットオブジェクト (DroppableStateSnapshot や DraggableStateSnapshot) を使用して、独自のスタイルを適用することも可能です。
“`jsx
// Droppable のスタイル例
const getListStyle = (isDraggingOver) => ({
backgroundColor: isDraggingOver ? ‘lightblue’ : ‘lightgrey’,
padding: 8,
width: 250,
minHeight: 500,
});
{(provided, snapshot) => (
{/ … Draggable items /}
{provided.placeholder}
)}
// Draggable のスタイル例
const getItemStyle = (isDragging, draggableStyle) => ({
userSelect: ‘none’,
padding: 16,
margin: ‘0 0 8px 0’,
backgroundColor: isDragging ? ‘#263B4A’ : ‘#456C86’,
color: ‘white’,
…draggableStyle, // provided.draggableProps.style を含める
});
{(provided, snapshot) => (
{item.content}
)}
“`
getItemStyle 関数の例のように、provided.draggableProps.style をカスタムスタイルにマージすることが推奨されます。これにより、react-beautiful-dnd がドラッグ中に適用する位置情報などのインラインスタイルが維持されます。
6. アクセシビリティ
react-beautiful-dnd の大きな特徴の一つは、アクセシビリティへの配慮です。マウス操作だけでなく、キーボードでの操作にも対応しており、スクリーンリーダーへの通知機能も備えています。
-
キーボード操作:
- スペースキー: フォーカスされた
Draggableアイテムを選択(ドラッグを開始)またはドロップします。 - 矢印キー (上下左右): ドラッグ中のアイテムを移動します。移動可能な位置(他の
Draggableの間やDroppableの端)を自動的に計算し、次の有効な位置にアイテムを移動させます。 - エスケープキー: ドラッグ操作をキャンセルし、アイテムを元の位置に戻します。
- スペースキー: フォーカスされた
-
スクリーンリーダー:
- アイテムが選択されたとき、移動したとき、ドロップされたときなどに、スクリーンリーダーに対して操作状況を通知するライブリージョンメッセージが自動的に生成されます。
- これらのメッセージはカスタマイズ可能です。
DragDropContextにonBeforeCapture,onBeforeDragStart,onDragStart,onDragUpdate,onDragEndなどのコールバックを設定し、その中でaria-live領域に独自のテキストコンテンツを挿入することで、よりアプリケーションに合わせた情報を提供できます。
react-beautiful-dnd は、これらのアクセシビリティ機能をデフォルトで有効にしています。特別な設定なしにある程度のキーボード操作やスクリーンリーダー対応が実現できるのは、このライブラリの大きな利点です。
7. パフォーマンス
大規模なリストや多数のアイテムを扱う場合、パフォーマンスが懸念事項となることがあります。react-beautiful-dnd は比較的パフォーマンスが高いですが、注意すべき点もあります。
-
不要な再レンダリングの回避:
DraggableやDroppableのレンダリングプロップの子関数は、ドラッグ操作の様々な段階で呼び出されます。スナップショットオブジェクトを使ってスタイルを動的に変更する際に、スタイル計算や重い処理を子関数内で行うと、頻繁な再レンダリングがパフォーマンスに影響を与える可能性があります。
スタイリングは、スナップショットの状態に基づいてシンプルなスタイルオブジェクトを返すようにするのが効率的です。
また、DraggableやDroppableの子コンポーネントが不要に再レンダリングされないように、React.memoやuseMemo,useCallbackを適切に使用することを検討してください。特にDraggableアイテムのコンポーネントは、ドラッグ操作中に頻繁に更新される可能性があるため、React.memoでメモ化するのが効果的です。 -
仮想化ライブラリとの連携:
非常に長いリスト(数百から数千アイテム)を扱う場合、全てのアイテムを同時にDOMにレンダリングするとパフォーマンスが大幅に低下します。このようなケースでは、react-windowやreact-virtualizedといった仮想化ライブラリの使用が考えられます。これらのライブラリは、画面に表示されているアイテムのみをレンダリングすることでパフォーマンスを向上させます。
しかし、react-beautiful-dndと仮想化ライブラリを組み合わせるのは容易ではありません。特に水平リスト、可変長のアイテム、グリッド、複数のDroppableを組み合わせた場合など、複雑なレイアウトでは実装が非常に困難になることがあります。react-beautiful-dndはアイテムの位置を正確に測定する必要があるため、仮想化によるDOMの動的な追加・削除と相性が悪い場合があります。
もし仮想化が必要なほど大規模なリストを扱う場合は、react-beautiful-dndの公式ドキュメントの仮想化に関するセクションをよく読み、制約を理解した上で慎重に実装を進めるか、あるいは@dnd-kitのような仮想化との連携がより考慮されている代替ライブラリを検討することも必要になるかもしれません。 -
onDragUpdateの利用:
onDragUpdateはドラッグ中に頻繁に呼び出されます。ここで複雑な計算やステート更新を行うとパフォーマンスの問題を引き起こす可能性があります。onDragUpdateは、ドラッグ中のアイテムの現在位置に基づいて他の要素の見た目を変更するなど、リアルタイムなUIフィードバックのために限定的に使用するのが望ましいです。ステートの更新(データの並び替え)は、ドラッグが終了したonDragEndでのみ行うべきです。
8. トラブルシューティングとFAQ
react-beautiful-dnd を使用していると、いくつか一般的な問題に遭遇することがあります。
-
“Invariant failed” エラー:
これはreact-beautiful-dndが内部で想定している条件が満たされなかった場合に発生するエラーです。よくある原因は以下の通りです。draggableIdやdroppableIdが重複している。Draggableのindexプロップが、その親Droppable内での実際のインデックスと一致していない。特にリストのステートを更新する際に、インデックスがずれてしまうことがあります。DraggableまたはDroppableのレンダリングプロップから返されるDOM要素に、provided.innerRef,provided.draggableProps,provided.dragHandleProps(またはprovided.droppableProps,provided.innerRef) が正しく適用されていない。provided.placeholderがDroppableの子要素として含まれていない。
エラーメッセージに詳細な情報が含まれていることが多いので、メッセージをよく読んで原因を特定してください。
-
スタイリングが意図通りに適用されない:
provided.draggableProps.styleをカスタムスタイルにマージするのを忘れていないか確認してください。react-beautiful-dndが適用する位置情報などのインラインスタイルが、カスタムスタイルで上書きされていないかチェックします。snapshotオブジェクト(isDragging,isDraggingOverなど)を使ってスタイルを条件付きで適用しているか確認してください。- CSSの優先順位の問題かもしれません。より詳細なセレクタや
!importantを一時的に使って原因を特定してみてください。
-
ドラッグが機能しない:
DragDropContextで全体をラップしているか確認してください。DroppableとDraggableの必須プロップ (droppableId,draggableId,index) が正しく設定されているか確認してください。特にIDの一意性とインデックスの正確性が重要です。provided.innerRef,provided.draggableProps,provided.dragHandleProps(またはprovided.droppableProps,provided.innerRef) が、レンダリングプロップから返される適切なDOM要素に適用されているか、スペルミスがないかなどを確認してください。- 特定の要素をドラッグハンドルに指定している場合 (
dragHandlePropsを特定の要素に適用)、その要素をクリック&ドラッグしているか確認してください。 isDragDisabledやisDropDisabledプロップが意図せずtrueになっていないか確認してください。StrictModeを使用している場合、開発モードでの二重レンダリングが問題を引き起こすことがあります。開発環境で一時的にStrictModeを無効にして動作を確認してみるのも一つの手です(ただし、本番環境では有効にすることをおすすめします)。
-
StrictModeで警告が出る:
react-beautiful-dndは内部でDOM操作を行いますが、ReactのStrictModeは副作用を検出するために開発モードでコンポーネントを二重にレンダリングすることがあります。これにより、不変条件(Invariant)に関する警告が出ることがありますが、通常これは開発モード特有のものであり、ライブラリの既知の動作です。公式ドキュメントでも言及されており、本番ビルドでは発生しません。基本的には無視しても問題ありません。ただし、他の原因による Invariant エラーではないか注意深く確認することは重要です。 -
モバイルデバイスでの動作:
react-beautiful-dndはタッチデバイスにも対応していますが、一部のデバイスやブラウザの組み合わせで予期しない動作をすることがあります。enableDefaultSensorsがtrueになっているか、タッチ操作がOSやブラウザによって阻害されていないかなどを確認してください。
トラブルシューティングの際は、React DevTools でコンポーネントツリーやステートを確認したり、ブラウザの開発者ツールでDOM構造やイベントリスナーを確認したりすることが非常に役立ちます。
9. react-beautiful-dnd の制限事項と代替ライブラリ
react-beautiful-dnd は特定のユースケース(垂直方向のリスト、グリッド)に特化しており、その範囲内では非常に強力で使いやすいライブラリです。しかし、全てのリッチなドラッグアンドドロップの要件を満たせるわけではありません。
制限事項:
- ツリー構造やネストされたDnD: アイテムがさらに子アイテムを含むようなツリー構造を持つリスト(フォルダのように展開・折りたたみできるリストなど)や、ドラッグ可能なアイテムの中にさらにドラッグ可能なアイテムが含まれるような構造には直接対応していません。
- 複雑なカスタムインタラクション: ドラッグ中にアイテムの形状が大きく変わったり、特別なアニメーションを伴ったり、複雑なインタラクション(例: ドラッグしながら別の要素と結合して新しい要素を作成)を実現するには、カスタマイズが難しい場合があります。
- 仮想化との連携: 前述の通り、大規模なリストでの仮想化との組み合わせは難易度が高いです。
- メンテナンス状況: Atlassian はかつてほど活発にメンテナンスを行っていないという状況があります。大きなバグ修正や新機能の追加ペースは以前よりも落ちている可能性があります。コミュニティによってメンテナンスされているフォークプロジェクト(例:
@hello-pangea/react-beautiful-dnd)も存在し、今後の開発はこちらが中心になる可能性もあります。
代替ライブラリ:
react-beautiful-dnd の制限がアプリケーションの要件と合わない場合、他のReact向けドラッグアンドドロップライブラリを検討する必要があります。
-
react-dnd:- より低レベルで、ドラッグアンドドロップ操作のロジック(状態管理、イベントハンドリング)を提供します。
- 見た目やインタラクションは全て自分で実装する必要があります。
- 柔軟性が非常に高く、リストだけでなく、ファイルドロップゾーン、コンポーネントの配置など、様々なD&Dシナリオに対応できます。
- 学習コストは
react-beautiful-dndより高い傾向があります。 - バックエンド(HTML5 backend, touch backendなど)を選択できます。
-
@dnd-kit:- 比較的新しいモダンなライブラリで、パフォーマンス、柔軟性、拡張性を重視しています。
- 仮想化ライブラリとの連携も比較的容易です。
- フックベースのAPIでHooks時代のReact開発になじみます。
- アクティブに開発されており、コミュニティも成長しています。
これらのライブラリはそれぞれ異なる設計思想と得意な分野を持っています。シンプルなリストやグリッドの並び替えに特化し、使いやすさとアクセシビリティを重視するなら react-beautiful-dnd が良い選択肢です。より複雑なD&Dパターンや高度なカスタマイズ、仮想化との連携が必要なら react-dnd や @dnd-kit を検討すると良いでしょう。
10. まとめ
この記事では、Reactでドラッグアンドドロップ機能を実装するための強力なライブラリ react-beautiful-dnd について、その基本的な使い方から詳細な機能、複数のリスト間移動のような応用例、さらにはアクセシビリティやパフォーマンス、トラブルシューティング、そして代替ライブラリについてまで、幅広く解説しました。
react-beautiful-dnd は、特にリストやグリッド形式のアイテムの並び替えにおいて、開発者が直面する多くの課題(DOM操作、アニメーション、ステート管理、アクセシビリティ)をエレガントに解決してくれます。DragDropContext、Droppable、Draggable の3つのコアコンポーネントと、レンダリングプロップを通じて提供される provided および snapshot オブジェクトを理解し、適切に活用することで、洗練されたドラッグアンドドロップ機能を比較的容易に実現できます。
一方で、ツリー構造のような複雑なレイアウトや、特定の仮想化ライブラリとの組み合わせ、あるいは非標準的なインタラクションの実装には制限があることも認識しておく必要があります。ご自身のアプリケーションの要件をよく検討し、react-beautiful-dnd が最適かどうかを判断することが重要です。もし要件が合わない場合は、react-dnd や @dnd-kit といった代替ライブラリも有力な選択肢となります。
この記事で得た知識を元に、ぜひ実際にコードを書いて react-beautiful-dnd を使ってみてください。理論だけでなく、実際に手を動かすことで理解が深まり、応用力が身につきます。
Reactアプリケーションに直感的で魅力的なドラッグアンドドロップ機能を組み込み、ユーザーエクスペリエンスをさらに向上させましょう!
11. 付録
- この記事で紹介した最小限のリスト実装と複数リスト実装のサンプルコードは、GitHubリポジトリのリンク (← サンプルコードを置いておく場合のリンク) にまとめてあります。
react-beautiful-dnd公式ドキュメント: https://react-beautiful-dnd.netlify.app/ (非常に詳細で役立ちます)react-beautiful-dndGitHubリポジトリ: https://github.com/atlassian/react-beautiful-dnd@hello-pangea/react-beautiful-dnd(コミュニティフォーク): https://github.com/hello-pangea/react-beautiful-dnd
上記で約5000語の記事として構成・執筆しました。コード例や詳細な説明を多く含め、読者が実際に実装できるような内容を目指しました。