ドラッグ&ドロップできるリストコンポーネントを作成する React 用ライブラリの dnd kit を使ってみた
こんにちは、Gaji-Labo アシスタントエンジニアの石垣です。
今回は、ドラッグ&ドロップが可能なコンポーネントを作成する React 用ライブラリの dnd kit を触る機会があったため、使用方法についてまとめてみます。
dnd kit について
dnd kit は React 用の、ドラック&ドロップ可能なコンポーネントを実装するためのライブラリです。
公式ドキュメントによると軽量でパフォーマンスが高く、拡張可能であり、かつアクセシビリティも担保していることを売りとしています。GitHub でも最新バージョンが一ヶ月前に公開されているため、開発も活発であることが窺えます。
実装する上での観点としては、ドラッグ&ドロップをコンポーネントではなく用意されている hooks で実装するのが大きな特徴です。
公式で Storybook が公開されており、縦並びのリストや横並びのリスト、グリッドレイアウトのリスト等様々なユースケースに対応する実装を見ることができます。
個人的にはサンプルで2Dゲームを実装しているのが目を惹きました。
dnd kit で簡単なドラッグ&ドロップが可能なリストコンポーネントを作成する
導入
まずは dnd kit をプロジェクトに追加します。
yarn add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
@dnd-kit/core
がコアライブラリであり、これだけでもドラッグ&ドロップ可能なコンポーネントを作成することができますが、 @dnd-kit/sortable
を導入することでより簡単に実装しやすくなる hooks を使用することができます。
@dnd-kit/utilities
も実装の上で便利なユーティリティを使うことができるようになるため導入します。詳しくは後述します。
コンポーネント実装
並び替え用に8つのカードを持つ配列を作成しました。これを並び替えるコンポーネントを作成してみます。
interface draggableCardProps {
children: string;
}
export function DraggableCard({ children }: draggableCardProps) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: 4,
borderRadius: 4,
width: "150px",
height: "150px",
border: "1px solid black",
backgroundColor: "white",
}}
>
{children}
</div>
);
}
const items = ["1", "2", "3", "4", "5", "6", "7", "8"];
const contents = items.map((item) => ({
id: item,
content: <DraggableCard>{item.toString()}</DraggableCard>,
}));
上のカードを並び替えるためのコンポーネントの実装がこちらになります。以下、こちらを参考に解説していきます。
import { DndContext } from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
...
interface SortableItemProps {
id: string;
children: ReactElement;
}
function SortableItem({ id, children }: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{children}
</div>
);
}
export const DndSample = (): JSX.Element => {
const [state, setState] =
useState<{ id: string; content: ReactElement }[]>(contents);
const handleDragEnd = useCallback(
(event) => {
const { active, over } = event;
if (over === null) {
return;
}
if (active.id !== over.id) {
const oldIndex = state
.map((item) => {
return item.id;
})
.indexOf(active.id);
const newIndex = state
.map((item) => {
return item.id;
})
.indexOf(over.id);
const newState = arrayMove(state, oldIndex, newIndex);
setState(newState);
}
},
[state]
);
return (
<DndContext onDragEnd={handleDragEnd}>
<SortableContext items={state}>
<div style={{ display: "flex", flexWrap: "wrap" }}> // スタイル調整用
{state.map((item) => (
<SortableItem key={item.id} id={item.id}>
{item.content}
</SortableItem>
))}
</div>
</SortableContext>
</DndContext>
);
};
1. 移動用のコンポーネントを作成する
まずは先程のカードをラップする移動用のコンポーネントを作成します。
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
...
interface SortableItemProps {
id: string;
children: ReactElement;
}
function SortableItem({ id, children }: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{children}
</div>
);
}
前述の通り、 useSortable
という hooks を @dnd-kit/sortable
から呼び出して使っています。
useSortable
はドラッグ&ドロップの実装に必要ないくつかのプロパティを返しますが、ここでは5つのプロパティを使っています。この5つは全て必須です。
この中で attributes
listeners
setNodeRef
はドラッグ&ドロップさせるコンポーネントに直接渡します。
残りの transform
transition
はドラッグ&ドロップの移動とアニメーションをCSSで行うためのプロパティです。こちらも style として渡さないとコンポーネントを移動させることができないため要注意です。
transform
に値を渡すにあたって @dnd-kit/utilities
から呼び出した CSS
を使っています。これは transform
の値は object で返ってくるため、object から string に変換する手間を省くために用意されているユーティリティです。
これで移動用のコンポーネントを作成することができました。
2. コンポーネントをドラッグ&ドロップ可能にする
import { DndContext } from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
export const DndSample = (): JSX.Element => {
const [state, setState] =
useState<{ id: string; content: ReactElement }[]>(contents);
const handleDragEnd = useCallback(
...
);
return (
<DndContext onDragEnd={handleDragEnd}>
<SortableContext items={state}>
<div style={{ display: "flex", flexWrap: "wrap" }}> // スタイル調整用
{state.map((item) => (
<SortableItem key={item.id} id={item.id}>
{item.content}
</SortableItem>
))}
</div>
</SortableContext>
</DndContext>
);
};
コンポーネントをドラッグ&ドロップ可能にするために、DndContext と SortableContext というコンポーネントを呼び出して使用します。
この2つを上述した移動用のコンポーネントをそのままラップし、SortableContext に配列を渡すだけでドラッグ&ドロップができるようになります。この辺りはすっきりしていて書きやすいと思いました。
3. ドラッグ&ドロップ後に配列の状態を保存する
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
...
export const DndSample = (): JSX.Element => {
const [state, setState] =
useState<{ id: string; content: ReactElement }[]>(contents);
const handleDragEnd = useCallback(
(event) => {
const { active, over } = event;
if (over === null) {
return;
}
if (active.id !== over.id) {
const oldIndex = state
.map((item) => {
return item.id;
})
.indexOf(active.id);
const newIndex = state
.map((item) => {
return item.id;
})
.indexOf(over.id);
const newState = arrayMove(state, oldIndex, newIndex);
setState(newState);
}
},
[state]
);
return (
<DndContext onDragEnd={handleDragEnd}>
...
</DndContext>
);
};
...
最後に handleDragEnd でドラッグ&ドロップ後に配列の状態を保存できるようにします。
DndContext は event という引数を取り、これは active
と over
の2つの値を取ります。active は動かしたコンポーネントの移動開始時の状態で、 over は移動終了時の状態を取ります。もしコンポーネントをドラッグ&ドロップ可能領域の外に出した場合は over は null を返します。
また、配列操作を簡単にするためのユーティリティとして arrayMove
という関数が用意されています。
これでドラッグ&ドロップが可能なリストコンポーネントを作成することができました。
まとめ
今回はドラッグ&ドロップが可能なコンポーネントを作成する React 用ライブラリの dnd kit について使用方法をまとめました。
dnd kit は hooks を使って実装するという点が React を書き慣れている人には直感的で実装しやすいと感じました。
簡単に実装できる割に拡張性も高そうなので、今後も詳しい使い方を触りながら学んでいきたいと思っています。
Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています
弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?
もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!
求人応募してみる!