react-beautiful-dnd でドラッグ&ドロップが可能なリストコンポーネントを作成する

こんにちは、Gaji-Labo アシスタントエンジニアの石垣です。

今回は、ドラッグ&ドロップが可能なリストコンポーネントを作成する React 用ライブラリの react-beautiful-dnd を触る機会があったため、使用方法についてまとめてみます。

今回の実装は CodeSandbox で触れるようになっていますので、是非ご覧いただければと思います。

実装方法

並び替え用に、以下のような3つのカードを持つデータを作成しました。これを使ってリストコンポーネントを実装してみます。

3つのカードが並んでいる様子のキャプチャ

CodeSandbox

interface draggableCardProps {
  children: string;
}

export function DraggableCard({ children }: draggableCardProps) {
  return (
    <div
      style={{
        borderRadius: 4,
        width: "100px",
        height: "100px",
        border: "1px solid black",
        margin: "4px"
      }}
    >
      {children}
    </div>
  );
}

export const items = [
  {
    id: "1",
    content: <DraggableCard>item 1</DraggableCard>
  },
  {
    id: "2",
    content: <DraggableCard>item 2</DraggableCard>
  },
  {
    id: "3",
    content: <DraggableCard>item 3</DraggableCard>
  }
];

以上で作成した items を並び替えられるようにしたコンポーネントの実装が以下になります。

CodeSandbox

import React, { useState, useCallback } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import type {
  DropResult,
  DroppableProvided,
  DraggableProvided
} from "react-beautiful-dnd";

function BeautifulDndExample() {
  const [state, setState] = useState(items);
  const handleDragEnd = useCallback(
    (result: DropResult) => {
      if (!result.destination) {
        return;
      }

      const newState = [...state];

      const [removed] = newState.splice(result.source.index, 1);
      newState.splice(result.destination.index, 0, removed);
      setState(newState);
    },
    [state]
  );

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="items">
        {(provided: DroppableProvided) => (
          <ul
            {...provided.droppableProps}
            ref={provided.innerRef}
            style={{ listStyleType: "none" }} // スタイル調整用
          >
            {state.map(({ id, content }, index) => {
              return (
                <Draggable key={id} draggableId={id} index={index}>
                  {(provided: DraggableProvided) => (
                    <li
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                    >
                      {content}
                    </li>
                  )}
                </Draggable>
              );
            })}
            {provided.placeholder}
          </ul>
        )}
      </Droppable>
    </DragDropContext>
  );
}

このコードを元に、順を追って説明していきます。

1. react-beautiful-dnd から必要なコンポーネントと型を import する

import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import type {
  DropResult,
  DroppableProvided,
  DraggableProvided
} from "react-beautiful-dnd";

まずは DragDropContext, Droppable, Draggable という3つのコンポーネントを import します。
それぞれ Draggable はドラッグ対象のコンポーネント、 Droppable はドラッグ&ドロップできる領域のコンポーネントになります。
それらを DragDropContext で囲むことでドラッグ&ドロップが可能になります。

また、TypeScript 使用時には DropResult などの型が標準で用意されているため、それらを import することで定義が可能です。

2. 並び替えするアイテムを Draggable に流し込み、 DragDropContext と Droppable の子要素にする

<DragDropContext onDragEnd={handleDragEnd}>
  <Droppable droppableId="items">
    {(provided: DroppableProvided) => (
      <ul
        {...provided.droppableProps}
        ref={provided.innerRef}
        style={{ listStyleType: "none" }} // スタイル調整用
      >
        {state.map(({ id, content }, index) => {
          return (
            <Draggable key={id} draggableId={id} index={index}>
              {(provided: DraggableProvided) => (
                <li
                  ref={provided.innerRef}
                  {...provided.draggableProps}
                  {...provided.dragHandleProps}
                >
                  {content}
                </li>
              )}
            </Draggable>
          );
        })}
        {provided.placeholder}
      </ul>
    )}
  </Droppable>
</DragDropContext>

先ほど作成したデータを map で Draggable の中に流し込み、それを DragDropContext と Droppable の子要素として置いています。

Droppable には droppableId 、Draggable には draggableId を指定するようにします。これにより、並び替えが可能になります。

また、Droppable と Draggable はそれぞれ provided という引数が返り値として渡ってきます。provided.innerRefprovided.draggableProps または provided.droppableProps は、以下のようにそれぞれのコンポーネントが持つ最も外側にある要素に指定する必要があります。

<Draggable key={id} draggableId={id} index={index}>
  {(provided: DraggableProvided) => (
    <li
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}
    >
      {content}
    </li>
  )}
</Draggable>
<Droppable droppableId="items">
  {(provided: DroppableProvided) => (
    <ul
      {...provided.droppableProps}
      ref={provided.innerRef}
      style={{ listStyleType: "none" }} // スタイル調整用
    >
      {state.map(({ id, content }, index) => {
        return (
          ...
        );
      })}
      {provided.placeholder}
    </ul>
  )}
</Droppable>

Droppable にのみ、 placeholder というプロパティも持っています。
これは Droppable のコンポーネントの末尾に置くことで、ドラッグ中に発生するスタイルの崩れを、ドラッグしているコンポーネントの幅を確保することで防いでくれるものです。

3. 並び替えた状態を保持する用の関数を設定する

最後に、handleDragEnd 関数を DragDropContextonDragEnd props に渡して、並び替えた後にその状態を保持できるようにします。

function BeautifulDndExample() {
  const [state, setState] = useState(items);
  const handleDragEnd = useCallback(
    (result: DropResult) => {
      if (!result.destination) {
        return;
      }

      const newState = [...state];

      const [removed] = newState.splice(result.source.index, 1);
      newState.splice(result.destination.index, 0, removed);
      setState(newState);
    },
    [state]
  );

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      ...
    </DragDropContext>
  );
}

これでドラッグ&ドロップで並び替えするコンポーネントを実装することができました。

並び替えた後、順番が保持されている

まとめ

以前にもドラッグ&ドロップを実装できる React 用ライブラリである react-smooth-dnd をご紹介しましたが、 react-beautiful-dnd はそれに比べると記法は複雑ですが拡張性が高く感じました。

素早く実装するなら react-smooth-dnd 、拡張性を求めるなら react-beautiful-dnd がそれぞれ適しているように思います。

今後の記事で、 react-beautiful-dnd をより拡張して使う方法についてもまとめたいと思います。

ドラッグ&ドロップを実装できる React 用ライブラリを探している方の参考になれば幸いです。

Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています

弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!

投稿者 Ishigaki Shotaro

アシスタントエンジニアとしてHTML/CSS/JavaScriptの実装やRailsの組み込み、スタイルガイドの構築などを担当しています。 業務の中でさまざまな学びを吸収しながら、文書構造やアクセシビリティに目を向けたマークアップの学習やJavaScriptの学習などを行っています。チームに貢献できるエンジニアとなるために日々奮闘中です。