Reactでドラッグアンドドロップ実装!react-beautiful-dnd 入門

はい、承知いたしました。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つのコンポーネントです。これらのコンポーネントを組み合わせることで、ドラッグ可能なアイテムとドロップ可能な領域を定義し、ドラッグアンドドロップのイベントを管理します。

  1. DragDropContext: ドラッグアンドドロップ機能を提供する範囲を定義する最も外側のコンポーネントです。アプリケーション全体、あるいは特定のセクションをこのコンポーネントでラップします。ドラッグイベントが発生した際に呼び出されるコールバック関数(onDragEnd など)を設定します。
  2. Droppable: アイテムをドロップできる領域(ドロップゾーン)を定義します。リストコンテナ、ボードの列などがこれに該当します。Droppable の子要素は、ドラッグ可能なアイテム(Draggable)を配置するための領域となります。Droppable はレンダリングプロップ(レンダー関数)パターンを使用し、子関数に providedsnapshot オブジェクトを渡します。
  3. Draggable: ドラッグ可能な個々のアイテムを定義します。リストの各項目、ボードの各カードなどがこれに該当します。Draggable もレンダリングプロップパターンを使用し、子関数に providedsnapshot オブジェクトを渡します。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 オブジェクトの役割、そして innerRefdroppablePropsdraggablePropsdragHandleProps を正しくDOM要素に適用することが重要です。

2. 環境構築とインストール

react-beautiful-dnd を使うための環境を準備しましょう。

前提条件:

  • Node.js と npm または yarn がインストールされていること。
  • Reactの基本的な知識があること。

ステップ:

  1. 新しい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
  2. 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;
“`

コードの説明:

  1. initialData: ドラッグアンドドロップするアイテムの初期データです。各アイテムには一意の id と表示する content が必要です。
  2. reorder 関数: これは配列内の要素を並び替えるためのヘルパー関数です。splice メソッドを使って、指定された開始インデックスの要素を削除し、終了インデックスに挿入しています。これは onDragEnd で使用されます。
  3. useState(initialData): リストの現在の状態を管理するためにReactの useState フックを使用しています。
  4. onDragEnd 関数: DragDropContext の必須プロップです。ドラッグ操作が終了したときに呼び出されます。引数 result には、ドラッグ操作に関する詳細情報(ドラッグされたアイテムのID、元の位置、ドロップ先の位置など)が含まれています。
    • !result.destination: アイテムがドロップ可能な領域外にドロップされた場合は何もせずに関数を終了します。
    • result.source.index === result.destination.index: アイテムが元の位置と同じ場所にドロップされた場合も何もせずに関数を終了します。
    • reorder(...): ドロップ先が有効な場合、reorder 関数を使って items 配列の要素を並び替えます。
    • setItems(newItems): 並び替えられた新しい配列でステートを更新します。Reactはステートの変更を検知してUIを再レンダリングし、リストの表示が新しい順序に更新されます。
  5. DragDropContext onDragEnd={onDragEnd}: ドラッグアンドドロップ機能の最上位コンテナです。onDragEnd プロップに関数ハンドラーを渡します。
  6. 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 の子要素リストの最後にレンダリングする必要があります。
  7. 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、ドラッグされたアイテムのID draggableId、アイテムのタイプ type など)が含まれます。この関数内でアプリケーションのステート(データの並び順)を更新する処理を行います。
  • canCaptureTrailingSpace?: boolean: ドラッグ中の要素の後ろに余分なスペースをキャプチャするかどうか。特定のレイアウトで役立つ場合があります。デフォルトは false
  • enableDefaultSensors?: boolean: デフォルトの入力センサー(マウス、キーボード、タッチ)を有効にするかどうか。通常は true のまま使用します。カスタムセンサーを使用する場合に false にします。

onDragEndresult: 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.sourceresult.destination です。これらの情報を使って、どのリストの何番目のアイテムが、どのリストの何番目に移動したかを判断し、ステートを更新します。

4.2. Droppable

Droppable はアイテムをドロップできる領域を定義します。主なプロップは以下の通りです。

  • droppableId: string: 必須プロップDragDropContext 内で一意である必要があります。onDragEndresult オブジェクトでこの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: プレースホルダーが使用されているかどうか。

これらのスナップショットプロパティを使って、例えば isDraggingOvertrue のときに Droppable の背景色を変えるなどのスタイリングを行うことができます。

4.3. Draggable

Draggable はドラッグ可能な個々のアイテムを定義します。主なプロップは以下の通りです。

  • draggableId: string: 必須プロップDragDropContext 内(より正確には同じ typeDroppable 内)で一意である必要があります。onDragEndresult オブジェクトでこの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': 現在のドラッグモードです。

これらのスナップショットプロパティを使って、例えば isDraggingtrue のときに 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) の説明:

  1. startColumnendColumn を、ドラッグ元 (source.droppableId) とドラッグ先 (destination.droppableId) のIDを使って boardData.columns から取得します。
  2. 同じカラム内での並び替え:
    • startColumn.id === endColumn.id の場合に実行されます。
    • reorder 関数を使用して、そのカラムの taskIds 配列内の要素を並び替えます。
    • 並び替えられた newTaskIds を含む新しい newColumn オブジェクトを作成します。
    • boardData オブジェクトをスプレッドし、columns プロパティ内の該当するカラムを新しい newColumn で更新した newBoardData を作成します。
    • setBoardData でステートを更新します。
  3. 異なるカラム間での移動:
    • startColumn.id !== endColumn.id の場合に実行されます。
    • move 関数を使用します。この関数は、ソースカラムの taskIds からアイテムを削除し、デスティネーションカラムの taskIds にアイテムを追加します。結果として、ソースとデスティネーション両方の新しい taskIds 配列を含むオブジェクトを返します。
    • resultMove オブジェクトを使って、新しい newStartColumnnewEndColumn オブジェクトを作成します。
    • boardData オブジェクトをスプレッドし、columns プロパティ内のソースカラムとデスティネーションカラムを新しいオブジェクトで更新した newBoardData を作成します。
    • setBoardData でステートを更新します。

この実装により、タスクカードをTodo、Doing、Doneのカラム間で自由にドラッグアンドドロップできるようになります。

5.2. 水平方向のリスト

Droppabledirection プロップを "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.draggablePropsprovided.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>

この場合、draggablePropsDraggable のルート要素(この例では一番外側の div)に適用しますが、実際にドラッグを開始するには dragHandleProps が適用された内部の div をクリック&ドラッグする必要があります。

5.4. カスタムスタイリング

react-beautiful-dnd はドラッグ中やドロップオーバー時などに自動でスタイルを適用しますが、スナップショットオブジェクト (DroppableStateSnapshotDraggableStateSnapshot) を使用して、独自のスタイルを適用することも可能です。

“`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 の端)を自動的に計算し、次の有効な位置にアイテムを移動させます。
    • エスケープキー: ドラッグ操作をキャンセルし、アイテムを元の位置に戻します。
  • スクリーンリーダー:

    • アイテムが選択されたとき、移動したとき、ドロップされたときなどに、スクリーンリーダーに対して操作状況を通知するライブリージョンメッセージが自動的に生成されます。
    • これらのメッセージはカスタマイズ可能です。DragDropContextonBeforeCapture, onBeforeDragStart, onDragStart, onDragUpdate, onDragEnd などのコールバックを設定し、その中で aria-live 領域に独自のテキストコンテンツを挿入することで、よりアプリケーションに合わせた情報を提供できます。

react-beautiful-dnd は、これらのアクセシビリティ機能をデフォルトで有効にしています。特別な設定なしにある程度のキーボード操作やスクリーンリーダー対応が実現できるのは、このライブラリの大きな利点です。

7. パフォーマンス

大規模なリストや多数のアイテムを扱う場合、パフォーマンスが懸念事項となることがあります。react-beautiful-dnd は比較的パフォーマンスが高いですが、注意すべき点もあります。

  • 不要な再レンダリングの回避:
    DraggableDroppable のレンダリングプロップの子関数は、ドラッグ操作の様々な段階で呼び出されます。スナップショットオブジェクトを使ってスタイルを動的に変更する際に、スタイル計算や重い処理を子関数内で行うと、頻繁な再レンダリングがパフォーマンスに影響を与える可能性があります。
    スタイリングは、スナップショットの状態に基づいてシンプルなスタイルオブジェクトを返すようにするのが効率的です。
    また、DraggableDroppable の子コンポーネントが不要に再レンダリングされないように、React.memouseMemo, useCallback を適切に使用することを検討してください。特に Draggable アイテムのコンポーネントは、ドラッグ操作中に頻繁に更新される可能性があるため、React.memo でメモ化するのが効果的です。

  • 仮想化ライブラリとの連携:
    非常に長いリスト(数百から数千アイテム)を扱う場合、全てのアイテムを同時にDOMにレンダリングするとパフォーマンスが大幅に低下します。このようなケースでは、react-windowreact-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 が内部で想定している条件が満たされなかった場合に発生するエラーです。よくある原因は以下の通りです。

    • draggableIddroppableId が重複している。
    • Draggableindex プロップが、その親 Droppable 内での実際のインデックスと一致していない。特にリストのステートを更新する際に、インデックスがずれてしまうことがあります。
    • Draggable または Droppable のレンダリングプロップから返されるDOM要素に、provided.innerRef, provided.draggableProps, provided.dragHandleProps (または provided.droppableProps, provided.innerRef) が正しく適用されていない。
    • provided.placeholderDroppable の子要素として含まれていない。
      エラーメッセージに詳細な情報が含まれていることが多いので、メッセージをよく読んで原因を特定してください。
  • スタイリングが意図通りに適用されない:

    • provided.draggableProps.style をカスタムスタイルにマージするのを忘れていないか確認してください。react-beautiful-dnd が適用する位置情報などのインラインスタイルが、カスタムスタイルで上書きされていないかチェックします。
    • snapshot オブジェクト(isDragging, isDraggingOver など)を使ってスタイルを条件付きで適用しているか確認してください。
    • CSSの優先順位の問題かもしれません。より詳細なセレクタや !important を一時的に使って原因を特定してみてください。
  • ドラッグが機能しない:

    • DragDropContext で全体をラップしているか確認してください。
    • DroppableDraggable の必須プロップ (droppableId, draggableId, index) が正しく設定されているか確認してください。特にIDの一意性とインデックスの正確性が重要です。
    • provided.innerRef, provided.draggableProps, provided.dragHandleProps (または provided.droppableProps, provided.innerRef) が、レンダリングプロップから返される適切なDOM要素に適用されているか、スペルミスがないかなどを確認してください。
    • 特定の要素をドラッグハンドルに指定している場合 (dragHandleProps を特定の要素に適用)、その要素をクリック&ドラッグしているか確認してください。
    • isDragDisabledisDropDisabled プロップが意図せず true になっていないか確認してください。
    • StrictMode を使用している場合、開発モードでの二重レンダリングが問題を引き起こすことがあります。開発環境で一時的に StrictMode を無効にして動作を確認してみるのも一つの手です(ただし、本番環境では有効にすることをおすすめします)。
  • StrictMode で警告が出る:
    react-beautiful-dnd は内部でDOM操作を行いますが、Reactの StrictMode は副作用を検出するために開発モードでコンポーネントを二重にレンダリングすることがあります。これにより、不変条件(Invariant)に関する警告が出ることがありますが、通常これは開発モード特有のものであり、ライブラリの既知の動作です。公式ドキュメントでも言及されており、本番ビルドでは発生しません。基本的には無視しても問題ありません。ただし、他の原因による Invariant エラーではないか注意深く確認することは重要です。

  • モバイルデバイスでの動作:
    react-beautiful-dnd はタッチデバイスにも対応していますが、一部のデバイスやブラウザの組み合わせで予期しない動作をすることがあります。enableDefaultSensorstrue になっているか、タッチ操作が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操作、アニメーション、ステート管理、アクセシビリティ)をエレガントに解決してくれます。DragDropContextDroppableDraggable の3つのコアコンポーネントと、レンダリングプロップを通じて提供される provided および snapshot オブジェクトを理解し、適切に活用することで、洗練されたドラッグアンドドロップ機能を比較的容易に実現できます。

一方で、ツリー構造のような複雑なレイアウトや、特定の仮想化ライブラリとの組み合わせ、あるいは非標準的なインタラクションの実装には制限があることも認識しておく必要があります。ご自身のアプリケーションの要件をよく検討し、react-beautiful-dnd が最適かどうかを判断することが重要です。もし要件が合わない場合は、react-dnd@dnd-kit といった代替ライブラリも有力な選択肢となります。

この記事で得た知識を元に、ぜひ実際にコードを書いて react-beautiful-dnd を使ってみてください。理論だけでなく、実際に手を動かすことで理解が深まり、応用力が身につきます。

Reactアプリケーションに直感的で魅力的なドラッグアンドドロップ機能を組み込み、ユーザーエクスペリエンスをさらに向上させましょう!

11. 付録


上記で約5000語の記事として構成・執筆しました。コード例や詳細な説明を多く含め、読者が実際に実装できるような内容を目指しました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール