はじめに
Next.jsを触ってみたいと、学習用に簡単なアプリを作っており、その中で新たに試したことを記事にしています。
同じようにこれからNext.js始めてみようという方の参考になれば嬉しいです。
やりたいこと
dndライブラリについて、どのようなものか知りたい。
今回は基本的な部分のみの記事です。
「はじめてのdnd-kit」シリーズ全4回の1回目です。
続きはこちら
環境
下記のDocker開発環境にて行います。
DnDライブラリについて
Next.jsで使えるDnDライブラリの選択肢がいくつかありました。
1. react-beautiful-dnd
- 特徴:シンプルで美しいUIが再現出来る
- 公式サイト:react-beautiful-dnd
2. react-dnd
- 特徴:細かい制御が可能。その分、DnDのロジックを自分で制御する必要がある
- 公式サイト:react-dnd
3. React Flow
- 特徴:ビジュアルエディタやフロー構築ツールに適したライブラリ。エッジを接続したりするインタラクティブなグラフ作成に最適
- 公式サイト:React Flow
4. dnd-kit
- 特徴:軽量でモダンなDnDライブラリ。低レベルAPIと高レベルAPIの両方を提供しており、シンプルなDnDから複雑なDnDインターフェースまでカバーできる
- 公式サイト:dnd-kit
今回の選択
- react-beautiful-dnd: メンテナンスが終了している
- react-dnd: ロジック制御が必要(初めてなのでよりシンプルなものを希望)
- React Flow: フロー構築向け(用途違い)
→今回は軽量なのと、シンプルな実装も可能なdnd-kitを使うことに決めました。
dnd-kitについて
特徴
詳細については、公式サイトに記載されていますので割愛しますが、良いなと思った点は下記です。
- 軽量:ライブラリのコア部分は約10KBとものすごく軽量でサクサク動く
- 依存関係なし:外部の依存関係がない状態で動作するように設計されているため、他のパッケージの更新や互換性問題を気にする必要がない
- Reactに最適化:useDraggableやuseDroppableなどのカスタムフックが予め準備されており、機能を簡単に組み込むことができる
基本コンポーネントとフック
DndContext
ドラッグ&ドロップの設定を行うためのコンテキスト。
全てのドラッグ&ドロップ操作はこのコンテキストの中で行われるため、これで全体を囲みドラッグ&ドロップ機能を有効にします。
useDroppable
ドロップ可能な領域を作成するためのフック。
これを使って、移動先(カードの受け入れ先)の領域を作成します。
useDraggable
ドラッグ可能な要素を作成するためのフック。
これを使って、今回でいうところの動かすチケット(カード)を作成します。
イメージ
DndContextで全体を囲みその中に、useDroppable,useDraggableで作成したコンポーネントが入るイメージ。
<DndContext> // 全体を囲む
<Draggable /> // これをドラッグ出来る(動かせる)
<Droppable /> // ここにドロップ出来る(ここに移動出来る)
</DndContext>
クイックスタートでお試し
まずは簡単にクイックスタートに沿ってサンプルを作って試してみます。
インストール
以下のコマンドを実行して、@dnd-kit/core
パッケージをインストールします。
npm install @dnd-kit/core
Droppable.tsx
ドロップするコンポーネントからuseDroppableフックを使って作成します。
最低限idを渡す必要があり、idは全てのDroppableコンポーネントで一意にする必要があります。
返り値について
- setNodeRef:参照を設定する関数。操作したいコンポーネントのrefとして登録し、追跡して他のドラッグしてきた要素との衝突や交差を検出できるようにする
-
isOver:ドラッグ可能な要素がドロップ可能な要素の上に移動した時にTrueになる
それを使って、重なった時に色を変えたり大きさを変えたりして、ユーザーにドロップ可能であることのヒントを与えることができる
import {useDroppable} from '@dnd-kit/core';
type DroppableProps = {
children: React.ReactNode;
id: string;
isOverAddClass?: string;
};
export default function Droppable({children, id, isOverAddClass}: DroppableProps) {
const {isOver, setNodeRef} = useDroppable({
id: id,
});
return (
<div ref={setNodeRef} className={isOver && isOverAddClass ? isOverAddClass : ""}>
{children}
</div>
);
}
Draggable.tsx
続いてドラッグするコンポーネントをuseDraggableを使って作成します。
こちらも最低限idを渡す必要があり、全てのDraggableコンポーネントで一意にする必要があります。
返り値について
- setNodeRef:参照を設定する関数。こちらも操作したいコンポーネントのrefとして登録が必要
-
transform:Draggableアイテムが選択されると、画面上でアイテムを移動するために必要な座標が設定される
これを使って、ドラッグ移動を描画できる - attributes:Draggableアイテムに適した一連のデフォルト属性を提供してくれる
- listeners:ドラッグ操作のイベントリスナーを含むため、操作したいコンポーネントに登録する
import {useDraggable} from '@dnd-kit/core';
import {CSS} from '@dnd-kit/utilities';
type DraggableProps = {
children: React.ReactNode;
id: string;
};
export default function Draggable({children, id}: DraggableProps) {
const {attributes, listeners, setNodeRef, transform} = useDraggable({
id: id,
});
const style = {
transform: CSS.Translate.toString(transform),
}
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{children}
</button>
);
}
組み立てる
DndContextの中に、作成したDroppable、Draggableコンポーネントを組み込みます。
クイックスタートのものをそのまま使いました。
クライアントコンポーネントにしないと動かせないので"use client"を付けてます。
"use client";
import { DndContext, DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import Droppable from '@/components/dnd_kit/Droppable';
import Draggable from '@/components/dnd_kit/Draggable';
import { useState } from 'react';
export default function Page() {
const containers = ['A', 'B', 'C'];
const [parent, setParent] = useState<UniqueIdentifier | null>(null);
const draggableMarkup = (
<Draggable id="draggable">
<div className='cursor-grab w-48 h-20 bg-blue-200 flex justify-center items-center'>
Drag me
</div>
</Draggable>
);
return (
<DndContext onDragEnd={handleDragEnd}>
<div className='flex flex-col justify-center items-center w-screen h-screen gap-8'>
// ドラッグするアイテム
<div className='flex h-20'>
{parent === null ? draggableMarkup : null}
</div>
// ドロップするコンテナ3個横並び
<div className='flex'>
{containers.map((id) => (
<Droppable key={id} id={id} isOverAddClass="bg-green-700">
<div className='w-52 h-24 border-2 border-dashed border-gray-100/50 flex justify-center items-center'>
{parent === id ? draggableMarkup : 'Drop here'}
</div>
</Droppable>
))}
</div>
</div>
</DndContext>
);
function handleDragEnd(event: DragEndEvent) {
const {over} = event;
setParent(over ? over.id : null);
}
}
前準備
1. ドロップエリアのコンテナとしてA、B、C を配列で準備
2. ドラッグしたコンポーネントがどこのコンテナに入ったかを管理するparentをuseStateで作成
3. 前項で作っておいたDraggable を使って、draggableMarkupを準備
組み立て
- 一番外側はDndContextで囲む
onDragEndで、ドラッグしたアイテムがドロップされた後に実行する処理を設定できる。
eventの返り値として受け取っているover
はDraggableがDraggable上でドロップされなければnull
となる。Draggable上でドロップされれば、ドロップ先のコンテナのid
を含むので、parent
へセット - ドラッグするアイテムを配置する領域は、parentがnull(Draggable上にドロップされていない状態)の時はドラッグするアイテムとしてdraggableMarkupを配置。すでにDraggable上にドロップいれば、何も表示しない
- ドロップエリアは3個コンテナ(エリア)を並べるため、map関数を利用し、idにはそれぞれのコンテナの値(A,B,C)を渡す。
parent(ドロップ先のコンテナのid)が自身のコンテナidと一致すれば、draggableMarkupを配置。
※上記2か3のいずれかにdraggableMarkupが表示されることになる
テスト
このような画面になりました。
DraggableがDraggableに重なった状態(isOverがtrueの状態)では、設定していた背景色が変わりました。
3つのコンテナそれぞれ、ドロップ出来ました。
併せて、枠外にドラッグ&ドロップした場合は、無事初期状態に戻すことが出来ました。
さいごに
・とりあえず、基本中の基本の部分だけは、使い方が分かりました。
・次回はdnd-kit/sortableを使って並び替えを実装してみます。
つづき
参考