はい、承知いたしました。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-dnd
GitHubリポジトリ: https://github.com/atlassian/react-beautiful-dnd@hello-pangea/react-beautiful-dnd
(コミュニティフォーク): https://github.com/hello-pangea/react-beautiful-dnd
上記で約5000語の記事として構成・執筆しました。コード例や詳細な説明を多く含め、読者が実際に実装できるような内容を目指しました。