Reactでテーブルの行の並び替えをライブラリを使って実装したくないですか?そんな時はReactの人気Drag&Dropライブラリの一つdnd-kitをぜひ試してください。動作確認にはシンプルなコードを利用しているので本文書通りに設定を行えば簡単に並び替えのできるテーブルを実装することができます。

Headless UIのテーブル専用のライブラリであるTanstack Tableの列の並び替えのDrag and Dropではdnd-kit以外のライブラリも利用できますがお勧めライブラリとしてdnd-kitが挙げられています。他のライブラリでも利用されることからも使いやすいライブラリであることがわかります。
fukidashi
テーブル以外の要素でも並び替え可能です。
fukidashi

プロジェクトの作成

Viteを利用してTypeScriptのReact環境を構築します。ディレクトリには任意の名前をつけることができるのでここではreact-dnd-kit-tableとしています。


npm create vite@latest
√ Project name: ... react-dnd-kit-table
√ Select a framework: » React
√ Select a variant: » TypeScript

Scaffolding project in C:\Users\Reffect\Desktop\react-dnd-kit-table...

Done. Now run:

  cd react-dnd-kit-table
  npm install
  npm run dev

プロジェクトを作成後、react-dnd-kit-tableディレクトリに移動してnpm installコマンドを実行してJavaScriptライブラリのインストールを行います。

ライブラリを利用前のテーブル表示

ライブラリを利用する前と後でどれくらいのコードの書き換えが必要なのか確認するためにライブラリを利用せずテーブルを表示させます。テーブルを表示するデータは無料のJSONPlaceHolderを利用します。

srcディレクトリのApp.tsxファイルに以下のコードを記述します。


import { useEffect, useState } from "react";
import Row from "./components/Row";

export type User = {
  id: number;
  name: string;
  email: string;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    const getUsers = async () => {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/users"
      );
      const data = await response.json();
      setUsers(data);
    };

    getUsers();
  }, []);

  return (
    <div style={{ margin: "2em" }}>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <Row key={user.id} user={user} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default App;

JSONPlaceHolderのhttps://jsonplaceholder.typicode.com/usersにアクセスを行い10件分のユーザデータを取得しています。取得したデータはuseState Hookで定義したusers変数に保存してmap関数で展開しRowコンポーネントでブラウザへの表示設定を行っています。

user情報をpropsで受け取るRowコンポーネントはcomponentsディレクトリを作成してRow.tsxファイルとして作成します。


import type { User } from "../App";

type Props = {
  user: User;
};

const Row = ({ user }: Props) => {
  return (
    <tr>
      <td>{user.id}</td>
      <td>{user.name}</td>
      <td>{user.email}</td>
    </tr>
  );
};

export default Row;

npm run devで開発サーバを起動してブラウザからアクセスすると罫線はありませんがテーブルが表示されます。

userテーブルの表示
userテーブルの表示

これからライブラリを利用してテーブル行の並び替え機能を実装していきます。

dnd-kitライブラリのインストール

dnd-kitを利用するための@dnd-kitのcoreライブラリとドラッグによる並び替えに必要となるsortableライブラリのインストールを行います。


npm install @dnd-kit/core @dnd-kit/sortable

並び替え機能の実装

DndContextの設定

まず最初にdnd-kitではDraggableやDroppableの要素をDndContextの内側に入れる必要があります。DndContextは名前にContextと入っている通りReactのContext APIを利用しています。Context APIを利用してDraggable(ドラッグする要素)やDroppable(ドラッグした要素がドロップされる)の要素間でデータの共有を行っています。


import { DndContext } from "@dnd-kit/core";
//略
<div style={{ margin: "2em" }}>
    <DndContext>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <Row key={user.id} user={user} />
          ))}
        </tbody>
      </table>
    </DndContext>
  </div>

DndContextを設定してもテーブルに変化はありません。

SortableContextの設定

次に並び替えを行う要素の外側にはSortableContextを設定します。propsで並び替えを行うデータを渡します。


import { DndContext } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
//略
<div style={{ margin: "2em" }}>
  <DndContext>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Email</th>
        </tr>
      </thead>
      <tbody>
        <SortableContext items={users}>
          {users.map((user) => (
            <Row key={user.id} user={user} />
          ))}
        </SortableContext>
      </tbody>
    </table>
  </DndContext>
</div>

SortableContextsを設定してもテーブルに変化はありません。

Draggable(&Droppable)の設定

ドラッグを行う要素の設定を行います。ここではドラッグに注目していますがSortableContextsの中で設定する要素はドラッグ可能な要素であり、ドロップも可能な要素です。

componentsディレクトリに作成したRow.tsxファイルを複製してDraggableRow.tsxファイルを作成します。


import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { User } from "../App";

type Props = {
  user: User;
};

const DraggableRow = ({ user }: Props) => {
  const { listeners, setNodeRef, transform } = useSortable({
    id: user.id,
  });

  const style = {
    transform: CSS.Transform.toString(transform),
  };

  return (
    <tr ref={setNodeRef} style={style} {...listeners}>
      <td>{user.id}</td>
      <td>{user.name}</td>
      <td>{user.email}</td>
    </tr>
  );
};

export default DraggableRow;

DraggableRowコンポーネントの中ではカスタムフックのuseSortable Hookから戻される関数やオブジェクトを利用します。

useSortable Hookの引数にはドラッグする要素を一意に識別するidを設定します。setNodeRefでドラッグする要素を参照するために設定します。listenersでドラッグアンドドロップの操作に関するリスナーを設定します。transformはドラッグの要素のドラッグの移動の座標情報が含まれておりこの値を利用して適切な場所に表示されます。setNodeRef, listeners, transformのいずれか一つが未設定だとドラッグできません。これ以外にもattributesやtransitionなどがuseSortableから戻されますが利用は必須ではありません。

ここまでの設定で要素のドラッグが行えるようになります。下記ではIDの7の要素を移動させています。

ドラッグの動作確認
ドラッグの動作確認

要素を動かしている間にブラウザのデベロッパーツールの要素を確認するとtransformのtranslate3dの値がドラッグに合わせて更新されることが確認できます。

style属性の値の変化
style属性の値の変化

現在の設定で要素のドラッグはできるようになりますが手を放すとドラッグした要素は元の場所に戻り、ドラッグして並び替えた情報を保持するためにイベントを設定します。

onDragOverイベントの設定

イベントはDndContextのpropsとして設定することができます。イベントにはonDragStart, onDragEnd, onDragOverなどがありますがドロップ可能な要素の上にドラッグ要素が移動した時に並び替えを行いたいのでonDragOverイベントを利用します。


import { DndContext, type DragOverEvent } from "@dnd-kit/core";
//略
const handleDragOver = (event: DragOverEvent) => {
  console.log("drag over", event);
};
//略
<DndContext onDragOver={handleDragOver}>

ドラッグするとonDragOverイベントが発火して、handleDragOver関数が実行されるとコンソールにeventの情報が表示されます。

onDragOverイベントで取得できるeventの確認
eventの中に含まれるactiveとoverオブジェクトの中にidがあり、activeはドラッグしている要素のidでoverはドラッグしている要素の下にあるドロップ可能な要素のidです。このidを並び替えに利用します。

並び替えを行えるようにhandleDragOver関数は以下のように更新します。並び替えはusers変数の配列の並び順を変更することで実現します。


import { arrayMove, SortableContext } from "@dnd-kit/sortable";
//略
const handleDragOver = (event: DragOverEvent) => {
  const { active, over } = event;

  if (over && active.id !== over.id) {
    setUsers((users) => {
      const oldIndex = users.findIndex((user) => user.id === active.id);
      const newIndex = users.findIndex((user) => user.id === over.id);
      return arrayMove(users, oldIndex, newIndex);
    });
  }
};

eventに含まれるactiveのidとoverのidの値が異なる場合のみ配列の更新を行います。activeのidとoverのidを利用してusers配列内での要素番号を取得しarrayMove関数を利用して並び替えを行っています。

設定後はドラッグした状態を保持することができるようになります。

id7の要素をid1の要素の下にドラッグをして移動させています。

Dragになる並び替え
Dragになる並び替え

要素の移動の制限

ドラッグする際に縦方向だけではなく横方向にもドラッグすることができます。

要素のドラッグ
要素のドラッグ

テーブルの行の並び替えの場合は横方向に移動する必要がないので縦方向のみのドラッグに限定するためにmodifiersのインストールを行います。


 % npm install @dnd-kit/modifiers

インストールしたmodifiersのrestrictToVerticalAxisをDndContextのmodifiers propsに設定します。


import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
//略
<DndContext
  onDragOver={handleDragOver}
  modifiers={[restrictToVerticalAxis]}
>

設定後は縦方向のみのドラッグになります。modifiersによる移動の制限は他にあるのでドキュメントを参考にしてください。

ブラウザをリロードすると並び替えはクリアにされるので通常は並び替え後にデータベースに保存する必要があります。

作成したコード

App.tsx


import { useEffect, useState } from "react";
import { DndContext, type DragOverEvent } from "@dnd-kit/core";
import { arrayMove, SortableContext } from "@dnd-kit/sortable";
import DraggableRow from "./components/DraggableRow";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";

export type User = {
  id: number;
  name: string;
  email: string;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    const getUsers = async () => {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/users"
      );
      const data = await response.json();
      setUsers(data);
    };

    getUsers();
  }, []);

  const handleDragOver = (event: DragOverEvent) => {
    const { active, over } = event;

    if (over && active.id !== over.id) {
      setUsers((users) => {
        const oldIndex = users.findIndex((user) => user.id === active.id);
        const newIndex = users.findIndex((user) => user.id === over.id);
        return arrayMove(users, oldIndex, newIndex);
      });
    }
  };

  return (
    <div style={{ margin: "2em" }}>
      <DndContext
        onDragOver={handleDragOver}
        modifiers={[restrictToVerticalAxis]}
      >
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Name</th>
              <th>Email</th>
            </tr>
          </thead>
          <tbody>
            <SortableContext items={users}>
              {users.map((user) => (
                <DraggableRow key={user.id} user={user} />
              ))}
            </SortableContext>
          </tbody>
        </table>
      </DndContext>
    </div>
  );
}

export default App;

components/DraggableRow.tsx


import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { User } from "../App";

type Props = {
  user: User;
};

const DraggableRow = ({ user }: Props) => {
  const { listeners, setNodeRef, transform } = useSortable({
    id: user.id,
  });

  const style = {
    transform: CSS.Transform.toString(transform),
  };

  return (
    <tr ref={setNodeRef} style={style} {...listeners}>
      <td>{user.id}</td>
      <td>{user.name}</td>
      <td>{user.email}</td>
    </tr>
  );
};

export default DraggableRow;

特定の要素のみDragできるように

tr要素のどの場所でもDragさせるのではなくid列のtd要素のみDrag可能にするには下記のようにtd要素にref={setActivatorNodeRef}と{…listeners}を設定します。


import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { User } from "../App";

type Props = {
  user: User;
};

const DraggableRow = ({ user }: Props) => {
  const { listeners, setNodeRef, transform, setActivatorNodeRef } = useSortable({
    id: user.id,
  });

  const style = {
    transform: CSS.Transform.toString(transform),
  };

  return (
    <tr ref={setNodeRef} style={style}>
      <td {...listeners}  ref={setActivatorNodeRef}>{user.id}</td>
      <td>{user.name}</td>
      <td>{user.email}</td>
    </tr>
  );
};

export default DraggableRow;