0. この記事について
自己学習でドラッグ&ドロップできるtodoアプリをNext.jsで作成しています。
その中でdnd-kitを初めて使用しました。
一旦実装はしたものの、正直があまり理解しきれてない部分も多いので、
1つのコンテナ内で要素を縦に並び替えるシンプルな機能を再実装しながら、基本機能を復習します。
1. dnd-kitとは
ドラッグ & ドロップ機能を実装できるReactのためのライブラリ。
特徴
主な特徴はこちらにもありますが、私も実装の中で下記のようなメリットは実感しました。
-
軽量
組み込みの React 状態管理とコンテキストを中心に構築されているため、ライブラリがスリム
ライブラリのコアは縮小すると約 10 KB になり軽量のため、パフォーマンスに与える影響を抑えられる -
外部の依存関係がない
他のパッケージの更新やバージョン不整合などを気にしなくて良い -
拡張性が高い
アニメーション、トランジション、動作、スタイルなど、幅広くカスタマイズできるので、必要に応じて独自の機能を追加可能
今回dnd-kitを採用した理由
ドラッグ & ドロップのライブラリ自体はdnd-kit以外にもあるのですが、
npm trendsでは現時点でdnd-kitが一番多くDLされています。
DL数が多い= そもそもライブラリとして勢いがある、またその分ドキュメントや技術記事が充実しており初学者にも取りかかりやすいのでは、と思いました。
2. 基本的な使い方(1つのコンテナ内で要素を縦に動かす)
前提
今回は下記のような1つのコンテナ内で複数のアイテムを縦に動かせる機能を実装します。
かなりシンプルですが、ディレクトリ構造は下記になります。
(App routerを使用したので、app
配下に作成していきます)
src/app/
├ page.tsx
└ component
└ DragItem.tsx
1. コアライブラリのインストール
まず下記コマンドで自身のReactプロジェクトにdnd-kitのコアライブラリをインストールします。
npm install @dnd-kit/core
2. DndContextを用意する
DndContextはアプリケーション内のドラッグ&ドロップの状態を管理し、必要な情報を子コンポーネントに渡します。
つまりドラッグ ・ドロップ可能要素は全てDndContextに囲まれている必要があるので、
まずはこちらを用意します。
'use client';
import React from 'react';
import { DndContext } from '@dnd-kit/core';
export default function Home() {
return (
<DndContext>
{/* ここにドラッグ・ドロップ可能要素が入る */}
</DndContext>
);
}
今回の機能はClient Component になるので、1行目に'use client';
を記載します。
3. SortableContextを用意する
要素のソートを行う為に必要です。ソート対象の要素はSortableContext
に囲まれている必要があるので、DndContext
の配下に用意します。
利用する際は下記dnd-kit/sortable
のインストールが必要です。
npm install @dnd-kit/sortable
インストールできたらDndContextの配下に追加します。
'use client';
import React, { useState } from 'react';
import { DndContext } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';
export default function Home() {
return (
<>
<DndContext>
<SortableContext>
{/* ここにドラッグ可能要素が入る */}
</SortableContext>
</DndContext>
</>
);
}
4. items用の配列を用意
SortableContextにはドラッグ対象要素のidを最低限渡す必要があります。
今回は複数のアイテムを用意したいので、シンプルに3つのidを用意し、また各要素に表示するテキストが入った配列を静的に追加しました。
後ほどこの配列は更新するのでuseState
で定義しておきます。
定義したitems
はSortableContext
のitems
に渡しておきます。
import React, { useState } from 'react';
{/* 中略 */}
export default function Home() {
const [items, setItems] = useState([
{
id: 1,
title: 'title1'
},
{
id: 2,
title: 'title2'
},
{
id: 3,
title: 'title3'
}
]);
return (
<>
<DndContext>
<SortableContext items={items}>
5. useSortableの追加
並び替え対象の要素に使用するフックです。
import React, { MouseEventHandler } from 'react';
import { useSortable } from "@dnd-kit/sortable";
export default function DragItem ({ id, title }) {
const { attributes, listeners, setNodeRef, transform } = useSortable({ id });
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined;
return (
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
{title}
</div>
);
}
useSortable
から受け取る各プロパティについて
-
attributes
ARIAやrole属性といったWAI-ARIAに当たる属性を渡してくれます。 -
setNodeRef
setNodeRef
はドラッグ&ドロップの対象となる要素を追跡し、
どの要素がドラッグされているのか、どこにドロップされるのかを伝えるために使われます。 -
listeners
マウスイベントやタッチイベントを検知するlistenerが渡ります。
これがないとドラッグを検知せず、要素が動かせなくなります。 -
style
ドラッグ対象要素が元々いた場所からの座標を返します。
ドラッグすると下記のようなオブジェクトが渡ってきます。
{x: -130, y: 114, scaleX: 1, scaleY: 1}
6. DragItemの組み込み
DragItemコンポーネントが完成したらDragItemも組み込みます。
DragItemは複数配置したいのでmapを使用して、手順4で用意しておいたid
とtitle
を受け取るようにします。
import DragItem from './component/DragItem';
{/* 中略 */}
return (
<>
<DndContext>
<SortableContext items={items}>
{items.map((item) => (
<DragItem key={item.id} id={item.id} title={item.title} />
))}
</SortableContext>
</DndContext>
</>
);
ここまで実装するとドラッグ自体は可能なのですが、並び替えをすることはまだできません。
onDragEndの定義が必要です。
7. onDragEnd処理を定義
onDragEndはドラッグしマウスを離した際の処理を定義します。
今回はitemDragEnd
の関数名にし、下記のように定義しました。
import { DndContext, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, arrayMove } from '@dnd-kit/sortable';
{/* 中略 */}
const itemDragEnd= (event : DragEndEvent) => {
const { active, over } = event;
if (!over == null || active.id === over.id) return;
const oldIndex = over.data.current?.sortable?.index;
const newIndex = active.data.current?.sortable?.index;
const newItems = arrayMove(items, oldIndex, newIndex);
setItems(newItems);
}
event
で受け取れるオブジェクトにactive
/ over
というキーがあり、それぞれ下記情報を受け取ります。
-
active
: ドラッグ中の要素の情報 -
over
: ドラッグ要素が接触した要素の情報
またarrayMove
はアイテムの順序を変更する関数で、@dnd-kit/sortable
からimportできます。
元の配列と、ドラッグ要素の変更前のindex、変更後のindexを渡すことで、新しい配列を作成する関数です。
その配列をuseState
(今回のソースだとsetItems
)を通して更新してあげることで、ドラッグ後の状態を実現できる、という流れです。
8. onDragEnd処理をDndContextへ追加
実装したitemDragEnd
をDndContextのonDragEnd
へ渡します。
{/* 中略 */}
return (
<>
<DndContext onDragEnd={itemDragEnd}>
9. 完成
これで各要素をドラッグ & ドロップができるようになっていると思います。
+α
ドラッグ要素にonClick属性をつけたい
そのままcomponentにonClick
を追加してもうまく動きません。
この場合はsensors
を使用して、「何px動かしたらドラッグを判定するか」を定義します。
まず@dnd-kit/core
からuseSensor
、 useSensors
、MouseSensor
をimportします。
import { DndContext, DragEndEvent, useSensor, useSensors, MouseSensor } from '@dnd-kit/core';
importしたsensor達は下記のように記述します。
これでマウスにより対象要素が5px(= distance
に設定した数値)以上動いた場合 = ドラッグされたと認識するようになります。
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { distance: 5 } })
)
作成したsensors
はDndContext
に渡します。
これでclickイベントにも反応してくれるようになります。
return (
<>
<DndContext onDragEnd={itemDragEnd} sensors={sensors}>
ドラッグ中のみ該当要素のstyleを変えたい
ドラッグ中であることが分かりやすいように見た目を変えることも可能です。
調べてみるとDragOverlay
なども紹介されていましたが、今回はドラッグ中の要素は背景色を変えたいだけなのでシンプルに実装しました。
DragItem
のuseSortable
からisDragging
を追加で受け取ります。
名前の通り、ドラッグ可能要素がドラッグされているとisDragging
はtrue
を返します。
const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id });
isDraging
でstyleを分岐させます。
これでドラッグ中の要素のみ背景色を変更することができます。
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
backgroundColor: isDragging ? '#ccc' : '#fff',
zIndex: isDragging ? 100 : 1,
} : undefined;
余談ですが、背景色を変えたところ、ドラッグ中の要素が他の要素の下に回り込んでしまうことに気がついたので、ドラッグ中の要素が常に上に来るようz-index
も追加で指定しました。
要素の該当部分をドラッグした時のみ動かしたい
ドラッグ要素にハンドルを用意し、そこをドラッグした際のみ要素が動かせるようにします。
DragItem.tsx
のuseSortable
からsetActivatorNodeRef
を受け取ります。
const { attributes, listeners, setNodeRef, transform, isDragging, setActivatorNodeRef } = useSortable({ id });
今回はDragHandle
componentを作成したので、こちらをドラッグした時のみ反応するようにします。(DragHandle
の中身はハンドルの見た目のみです)
そのラッパー要素に先ほどのsetActivatorNodeRef
と、元々外側のdiv
にあった{...listeners}
、{...attributes}
をハンドル側に移動します。
return (
<div ref={setNodeRef} style={style} onClick={onClick}>
{title}
<div ref={setActivatorNodeRef} {...listeners} {...attributes} >
<DragHandle />
</div>
</div>
);
{...listeners}
だけを移動しても動きますが、公式ドキュメント(こちら)に記載があるように、{...listeners}
がアタッチされている同じDOMにも、{...attributes}
をアタッチする必要があるようです。
ちなみに上記で紹介した方法を使って、ドラッグ中はbackground-color
を変更して、各アイテムにハンドルを用意した際の下記のような挙動になります。
ハンドル以外をドラッグした際はアイテムが動かず、右のドット箇所のみドラッグ可能になります。
まとめ・感想
今回初めてdnd-kitを使用してみましたが、思っていたよりもシンプルにドラッグ&ドロップの機能自体は実装することができました。
今回は復習として、1つのコンテナ内で要素を縦に動かすのみの挙動でしたが、実際に自己学習で作成した機能は複数コンテナ間での要素を動かす挙動なので、こちらもまたQiitaにまとめたいと思います。
参考