Drap & Dropを利用したリストの並び替え
2025-01-20
Drag & Drop はどんな時に使うの?
Drag & Drop のユースケースとして
- リスト表示の並び替え
- カラム表示の並び替え
- 画像やテキストにインプット
が一般的かと思います。
リスト表示の並び替え
今回は、「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>
);
};