[React + Typescript] react-beautiful-dnd を使ってドラッグ&ドロップ機能を実装する

2021.01.29

React + Typescript で実装されたフロントエンドにreact-beautiful-dndを使ったドラッグ&ドロップ機能を実装しました。

全体の流れ

本記事でやることの全体の流れです。

  • React + Typescript  で簡単な画像のリストを作成する
  • react-beautiful-dndを導入してリストのアイテムをドラッグ&ドロップできるようにする

成果物

React + Typescript プロジェクトの雛形を作成する

ドラッグ&ドロップを実装する前準備です。

create react app コマンドでプロジェクトの雛形を作成します。言語は Typescript を指定します。

npx create-react-app sample_dd --typescript

画像ファイルを用意する(Optional)

UI 上に表示する画像を準備します。今回はめそこスタンプをドラッグ&ドロップできるようにしました。

publicディレクトリ下にimagesフォルダを切ってその中に画像データを保持します。

charactersData.ts にドラッグ&ドロップするデータを登録

caractersData.tsに以下のデータを保持します。 idは後にreact-beautiful-dndを導入する際に必要になるので一意のものを付与してください。

export const CHARACTERS = [
  {
    id: "gambaruzoi",
    name: "がんばるぞい",
    thumb: "/images/1.png",
  },
  {
    id: "gyp",
    name: "ぎょぱー!",
    thumb: "/images/2.png",
  },
  {
    id: "iine",
    name: "いいね!",
    thumb: "/images/3.png",
  },
  {
    id: "shincyoku_doudesuka",
    name: "進捗どうですか",
    thumb: "/images/4.png",
  },
  {
    id: "shobon",
    name: "ショボーン",
    thumb: "/images/5.png",
  },
];

App.tsx にリストを追加する

header と ul 要素 を App.tsx に追加します。先ほど作成したcharactersData.tsをインポートして、map で要素を追加します。

import { CHARACTERS } from "./charactersData";
<div className="App">
  <header className="App-header">
    <h1>めそこスタンプ</h1>
    <ul className="characters">
      {CHARACTERS.map(({ id, name, thumb }) => {
        return (
          <li key={id}>
            <div className="characters-thumb">
              <img src={thumb} alt={`${name} Thumb`} />
            </div>
            <p>{name}</p>
          </li>
        );
      })}
    </ul>
  </header>
</div>

この時点で以下の画面が完成しました。このリスト内の要素をドラッグドロップで移動できるように実装を進めます。

react-beautiful-dnd ライブラリと型をインストール

react-beautiful-dndライブラリをインストールします。

yarn add @types/react-beautiful-dnd
yarn add react-beautiful-dnd

DragDropContext を App の root に追加する

import { DragDropContext } from "react-beautiful-dnd";

DragDropContextを React アプリのソースコードの最上位に追加することでreact-beautiful-dndがコンポーネントツリーにアクセスできるようになります。今回はApp.tsxに全てのコードを記述しているのでApp.tsxDragDropContextを追加します。

複数のコンポーネントでドラッグ&ドロップを利用する場合も同様で、ソースコードの最上位にDragDropContextが追加され、その下のコンポーネントをラップしている必要があります。

<div className="App">
    <header className="App-header">
        <h1>めそこスタンプ</h1>
        <DragDropContext>
            <ul className="characters">
            {CHARACTERS.map(({id, name, thumb}) => {
                return (
                <li key={id}>
                    <div className="characters-thumb">
                    <img src={thumb} alt={`${name} Thumb`} />
                    </div>
                    <p>
                    { name }
                    </p>
                </li>
                );
            })}
            </ul>
        <DragDropContext>
    </header>
</div>

ul 要素を Droppable エリアにする

import { DragDropContext, Droppable } from "react-beautiful-dnd";

次に、ドラッグしたアイテムをドロップできる範囲を追加します。 今回は<ul></ul>の範囲にアイテムをドロップできるようにしたいのでDroppableを以下の位置に追加します。

<div className="App">
    <header className="App-header">
        <h1>めそこスタンプ</h1>
        <DragDropContext>
            {/* Droppableをここに追加 */}
            <Droppable droppableId="characters">
            {(provided) => (
                <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
                {CHARACTERS.map(({id, name, thumb}) => {
                    return (
                    <li key={id}>
                        <div className="characters-thumb">
                        <img src={thumb} alt={`${name} Thumb`} />
                        </div>
                        <p>
                        { name }
                        </p>
                    </li>
                    );
                })}
                </ul>
            )}
            </Droppable>
        <DragDropContext>
    </header>
</div>

DroppableにはdroppableIdを追加します。これによりライブラリがこの特定の Droppable インスタンスを追跡できるようになります。

また、provided引数を渡す関数でラップする必要があり、この引数に含まれる値を元にどのアイテムがどの位置に移動されたかをトラッキングします。その部分が以下のコードになります。

<ul
  className="characters"
  {...provided.droppableProps}
  ref="{provided.innerRef}"
></ul>

リスト 内のアイテムをドラッグ可能にする Draggable を追加

import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

ドラッグ可能にしたいアイテムをDraggableコンポーネントでラップします。

Draggableも先ほどのDroppableと同様draggableIdを付与する必要があります。ここにはcharactersData.tsであらかじめ設定しておいたidを付与します。<li>に付与していたkeyプロパティもDraggableコンポーネントへ移動します。

また、<li>要素にもDraggableコンポーネントから渡されるprovided引数の以下のプロパティを付与します。

<li
  ref="{provided.innerRef}"
  {...provided.draggableProps}
  {...provided.dragHandleProps}
></li>
<div className="App">
    <header className="App-header">
        <h1>めそこスタンプ</h1>
        <DragDropContext>
            {/* Droppableをここに追加 */}
            <Droppable droppableId="characters">
            {(provided) => (
                <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
                {CHARACTERS.map(({id, name, thumb}, index) => {
                    return (
                        {/* Draggableをここに追加 */}
                        <Draggable key={id} draggableId={id} index={index}>
                        {(provided) => (
                            <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
                                <div className="characters-thumb">
                                <img src={thumb} alt={`${name} Thumb`} />
                                </div>
                                <p>
                                { name }
                                </p>
                            </li>
                        )}
                    </Draggable>
                    );
                })}
                </ul>
            )}
            </Droppable>
        <DragDropContext>
    </header>
</div>

ここまででドラッグ&ドロップ”っぽい”動きをするアプリができました。 ですが、このままでは実際に動かしてみるとドロップしたアイテムの順番が入れ替わってしまいます。

これは以下の2つの問題が原因で起こります。

  • アイテムをドラッグした際、そのアイテムが以前あった位置に次のアイテムが移動してしまう
  • React コンポーネントのレンダリングのタイミングでライブラリ内に保持されていたアイテムの順序情報が消えてしまう

placeholder を追加する

React Beautiful DnD が提供するplaceholderDroppable下に追加します。 placeholderを追加することで、ドラッグしたアイテムがドラッグされる前に使っていたスペースを埋めてくれるので、ドラッグした際にその下のアイテムがドラッグしたアイテムが以前あった場所に移動してしまう挙動が解決します。

    <div className="App">
      <header className="App-header">
        <h1>めそこスタンプ</h1>
        <DragDropContext>
         {/* Droppableをここに追加 */}
          <Droppable droppableId="characters">
          {(provided) => (
          <ul className="characters" {...provided.droppableProps} ref={provided.innerRef}>
            {CHARACTERS.map(({id, name, thumb}, index) => {
              return (
                {/* Draggableをここに追加 */}
                <Draggable key={id} draggableId={id} index={index}>
                  {(provided) => (
                    <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
                      <div className="characters-thumb">
                        <img src={thumb} alt={`${name} Thumb`} />
                      </div>
                      <p>
                        { name }
                      </p>
                    </li>
                  )}
                </Draggable>
              );
            })}
            {/* placeholderをここに追加 */}
            {provided.placeholder}
          </ul>
          )}
          </Droppable>
        </DragDropContext>
      </header>
    </div>

state と onDragEnd 関数を設定してドロップ後の List 内アイテムの順番を保持する

今の実装では、ドラッグ&ドロップしたアイテムが1つ前の位置に戻ってしまいます。これはドラッグ&ドロップした後、コンポーネントが再レンダリングされた時にreact-beautiful-dndのメモリ内に保持しているアイテムの順序が消えてしまうことが原因です。

これを回避するためにコンポーネントのstateに移動後のアイテムの情報を保持するように state を追加します。

さらに、アイテムがドロップされた後に実行される関数、onDragEndhandleOnDragEndという関数を渡して上記のstateを更新する処理を記述します。

import React, { useState } from "react";
import "./App.css";
import { CHARACTERS } from "./charactersData";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

function App() {
  const [characters, updateCharacters] = useState(CHARACTERS);
  function handleOnDragEnd(result: any) {
    const items = Array.from(characters);
    const [reorderedItem] = items.splice(result.source.index, 1);
    items.splice(result.destination.index, 0, reorderedItem);

    updateCharacters(items);
  }
  return (
    <div className="App">
      <header className="App-header">
        <h1>めそこスタンプ</h1>
        <DragDropContext onDragEnd={handleOnDragEnd}>
          <Droppable droppableId="characters">
            {(provided) => (
              <ul
                className="characters"
                {...provided.droppableProps}
                ref={provided.innerRef}
              >
                {characters.map(({ id, name, thumb }, index) => {
                  return (
                    <Draggable key={id} draggableId={id} index={index}>
                      {(provided) => (
                        <li
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                        >
                          <div className="characters-thumb">
                            <img src={thumb} alt={`${name} Thumb`} />
                          </div>
                          <p>{name}</p>
                        </li>
                      )}
                    </Draggable>
                  );
                })}
                {provided.placeholder}
              </ul>
            )}
          </Droppable>
        </DragDropContext>
      </header>
    </div>
  );
}

export default App;

これでreact-beautiful-dndを使ったドラッグ&ドロップ機能の実装が完了しました。

npm trendsで類似のライブラリを比べると一番利用されているのはreact-draggable、次いでreact-beautiful-dndreact-dndというデータが表示されました。

今回は特別なことは何もせず、リストに入ったアイテムを移動したいという簡単な要件を満たしたいだけだったので塾考せずにサンプルが多い&ドキュメントが可愛らしいという理由でreact-beautiful-dndを選択しましたが、ライブラリは要件に合わせて選出するのが良さそうです。

References