概要
HTML5 の Drag & Drop API(dragstart / dragover / drop イベント)を使い、外部ライブラリなしでリストアイテムをドラッグで並べ替える。dragIndex と hoverIndex を useRef で管理して順序を入れ替えるパターンを示す。
インストール
# 追加インストールは不要
実装
ドラッグ可能リストコンポーネント
// components/DraggableList.tsx
"use client";
import { useRef, useState } from "react";
type Item = { id: number; label: string };
type Props = {
initialItems: Item[];
};
export function DraggableList({ initialItems }: Props) {
const [items, setItems] = useState<Item[]>(initialItems);
const dragIndex = useRef<number | null>(null);
const [draggingId, setDraggingId] = useState<number | null>(null);
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
function handleDragStart(index: number, id: number) {
dragIndex.current = index;
setDraggingId(id);
}
function handleDragOver(e: React.DragEvent, index: number) {
e.preventDefault(); // drop を有効にするために必要
setHoverIndex(index);
}
function handleDrop(dropIndex: number) {
const from = dragIndex.current;
if (from === null || from === dropIndex) {
cleanup();
return;
}
setItems((prev) => {
const next = [...prev];
const [moved] = next.splice(from, 1);
next.splice(dropIndex, 0, moved);
return next;
});
cleanup();
}
function cleanup() {
dragIndex.current = null;
setDraggingId(null);
setHoverIndex(null);
}
return (
<ul className="w-full max-w-sm space-y-2">
{items.map((item, index) => (
<li
key={item.id}
draggable
onDragStart={() => handleDragStart(index, item.id)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={() => handleDrop(index)}
onDragEnd={cleanup}
className={`flex cursor-grab items-center gap-3 rounded-lg border px-4 py-3 text-sm
transition-all select-none
${draggingId === item.id ? "opacity-40" : ""}
${hoverIndex === index && draggingId !== item.id
? "border-blue-400 bg-blue-50"
: "border-gray-200 bg-white"
}
`}
>
<span className="text-gray-400">⠿</span>
<span className="text-gray-800">{item.label}</span>
</li>
))}
</ul>
);
}
使用例
// app/page.tsx
import { DraggableList } from "@/components/DraggableList";
const initialItems = [
{ id: 1, label: "タスク A" },
{ id: 2, label: "タスク B" },
{ id: 3, label: "タスク C" },
{ id: 4, label: "タスク D" },
{ id: 5, label: "タスク E" },
];
export default function Page() {
return (
<main className="flex min-h-screen flex-col items-center p-8">
<h1 className="mb-6 text-2xl font-bold">ドラッグで並べ替え</h1>
<DraggableList initialItems={initialItems} />
</main>
);
}
ポイント
draggable属性を要素に付けることで HTML5 DnD が有効になる。dragstartでドラッグ開始インデックスを記録するdragoverでe.preventDefault()を呼ばないとdropイベントが発火しない(重要)- インデックスの入れ替えは
splice(from, 1)で取り出し、splice(dropIndex, 0, moved)で挿入する方式 useRefでドラッグ中インデックスを管理することで、state 更新なしにドラッグ中の位置を追跡できる(再レンダリングを抑える)- 本番環境で複雑な DnD(グリッド間移動・タッチ対応等)が必要な場合は
@dnd-kit/core(アクセシビリティ対応あり)やreact-beautiful-dndを検討する