Drap & Dropを利用したリストの並び替え

2025-01-20
Drap & Dropを利用したリストの並び替えの画像

Drag & Drop はどんな時に使うの?

Drag & Drop のユースケースとして

  1. リスト表示の並び替え
  2. カラム表示の並び替え
  3. 画像やテキストにインプット

が一般的かと思います。

リスト表示の並び替え

今回は、「1. リスト表示の並び替え」について言及しますが、特にリスト表示はデータが多くなりやすいので、UXの改善に大きく役立つと考えています。

ボタン押下の上下の移動(orderの更新)で実装するのは簡単ですが、複数回のアクションが発生し、UXとしてはかなり悪いものになります。

あまりにアイテムが多いリストは、Drag & Drop では対応しきれないかもしれませんが、10〜30のアイテム数であれば、Drag & Drop を使用することで、UXを向上させることができます。

その点、Drag & Drop の実装難易度は高いかもしれませんが、覚えておくと分かりやすく付加価値を提供できる技術です。

10〜30のアイテム数であれば、Drag & Drop を使用することで、UXを向上できる 逆に、10〜30のアイテム数でボタン移動の実装をすると、UXが悪くなる可能性がある 付加価値を提供できる技術!

動作

実装

pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @dnd-kit/modifiers
import { DndContext, DragEndEvent } from "@dnd-kit/core";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { SortableContext } from "@dnd-kit/sortable";
import { arrayMoveImmutable } from "array-move";
import { useState } from "react";
import "remixicon/fonts/remixicon.css";
import { SortableItem } from "./SortableItem";

export type ItemType = {
  id: string;
  name: string;
  order: number;
};

// 前提として、order昇順でソートされている必要がある
const initialItems: Array<ItemType> = [
  { id: crypto.randomUUID(), name: "👊", order: 1 },
  { id: crypto.randomUUID(), name: "🍋", order: 2 },
  { id: crypto.randomUUID(), name: "🍍", order: 3 },
  { id: crypto.randomUUID(), name: "🍎", order: 4 },
  { id: crypto.randomUUID(), name: "🍗", order: 5 },
];

function App() {
  const [items, setItems] = useState<Array<ItemType>>(initialItems);

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;

    if (over == null || active.id === over.id) {
      return;
    }

    setItems((items) => {
      const oldIndex = items.findIndex((item) => item.id === active.id);
      const newIndex = items.findIndex((item) => item.id === over.id);
      // 配列の位置を変更する
      const newItems = arrayMoveImmutable(items, oldIndex, newIndex);
      // 配列の位置を更新後に order を更新する
      const updatedItems = newItems.map((item, index) => ({
        ...item,
        order: index + 1,
      }));
      return updatedItems;
    });
  };

  return (
    <div className="mx-auto max-w-4xl">
      <div className="flex flex-col">
        <DndContext
          // 縦方向のみの移動を許可する
          modifiers={[restrictToVerticalAxis]}
          onDragEnd={handleDragEnd}
        >
          <SortableContext items={items}>
            {items.map((item) => (
              <SortableItem key={item.id} item={item} />
            ))}
          </SortableContext>
        </DndContext>
      </div>
    </div>
  );
}

export default App;
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { ItemType } from "./App";
import clsx from "clsx";

type Props = {
  item: ItemType;
};

export const SortableItem = ({ item }: Props) => {
  const {
    setNodeRef,
    attributes,
    listeners,
    transform,
    transition,
    isDragging,
    setActivatorNodeRef,
  } = useSortable({ id: item.id });

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

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={clsx(
        "flex items-center gap-4 bg-white border border-gray-200 rounded-md p-4",
        isDragging && "z-10 shadow-md",
      )}
    >
      <button
        ref={setActivatorNodeRef}
        {...attributes}
        {...listeners}
        className={clsx("cursor-grab", isDragging && "cursor-grabbing")}
      >
        <i className={clsx(" ri-draggable text-xl")}></i>
      </button>
      <div className="">{item.order}</div>
      <div className="">{item.id}</div>
      <div className="">{item.name}</div>
    </div>
  );
};