【dnd-kit】Reactでドラッグアンドドロップ機能を実装するモダンなライブラリdnd-kitの使い方を解説
dnd-kitとは?
dnd-kitとは、Reactでドラッグアンドドロップを実装することができるライブラリの1つです。
dnd-kitは公式でサンプルを用意してくれているので、どんなようなものがあるのか知りたい方はまずこちらのサイトを参考にしてみてください。
なぜdnd-kitなのか
Reactでドラッグアンドドロップを提供しているライブラリだと、
- React DnD
- react-beautiful-dnd
などがあると思います。こららのライブラリに比べて比較的にとっかかりやすいのがdnd-kitだと考えています。
また、npm trendsを見てみてもdnd-kitはReact-DnDやreact-beautiful-dndに比べて人気は劣るものの、確実に差を詰めていることがわかります。今後はdnd-kitが主流になっていくのではないかと考えています。
参考:npm trends
ただ、dnd-kitは日本語での解説記事も少なく、Udemyなどの講座でもドラッグアンドドロップを実装する際にはReact DnDを使用していることが多いです。dnd-kitを使おうと考えているという方のために記事を作成しております。何かの参考になれば幸いです。
このページでは、dnd-kitを使ったドラッグアンドドロップ機能の実装方法を解説していきます。
dnd-kitに関する説明
dnd-kitの構成を理解する
実際にコードを書く前に、dnd-kitの構成を解説します。dnd-kitでは、useDraggable
フックとuseDroppable
フックを正しく動作させるために要素が<DndContext/>
で囲われている必要があります。
ドラッグ可能なアイテムとドロップ可能なアイテムは必ず<DndContext/>
で囲まれている必要があるということです。
具体的なコードの構成としては以下のようになります。
import React from 'react';
import { DndContext } from '@dnd-kit/core';
import { Draggable } from './Draggable';
import { Droppable } from './Droppable';
export const App = () => {
return (
<DndContext>
<Draggable />
<Droppable />
</DndContext>
)
}
実際に動作させたものはこちらから確認することができます。
DndContextについて理解する
<DndContext/
コンポーネントは、ドラッグもしくはドロップのコンポーネントとフック間でデータの共有をするために使用されます。 useDraggable
もしくは useDroppable
を使用するコンポーネントはDndContext内にある必要があります。
import React from 'react';
import { DndContext } from '@dnd-kit/core';
export const App = () => {
return (
<DndContext>
{/* useDraggable、`useDroppable` を使用するコンポーネント */}
</DndContext>
);
}
また、DndContext内にDndContextを含めることで互いに独立したインターフェースを実現することもできます。
DndContextには様々なイベントハンドラーが搭載されていますが、その詳細については以下の公式ドキュメントを参照してみてください。
useDroppableフックについて理解する
要素をある領域内にドロップさせるためには、 useDroppable
フックを使用します。 useDroppable
フックは以下のように使用します。
import {useDroppable} from '@dnd-kit/core';
function Droppable() {
const {setNodeRef} = useDroppable({
id: 'unique-id',
});
return (
<div ref={setNodeRef}>
{props.children}
</div>
);
}
Droppableコンポーネントには最低限、 useDroppable
フックが返す setNodeRef
関数を ref
属性を与える必要があります。この ref
属性がないと、領域やDOM間の衝突や交差などをうまく検知することができません。
また、すべてのDroppableコンポーネントは一意のID属性を保つ必要があります。また、Draggableな要素がDroppableな要素の上に来ると、 isOver
プロパティが true
になります。
もちろんDroppable領域は複数個作成できますが、 useDroppable
フックをその分だけ使用しなければなりません。
function MultipleDroppables() {
const {setNodeRef: setFirstDroppableRef} = useDroppable({
id: 'droppable-1',
});
const {setNodeRef: setsecondDroppableRef} = useDroppable({
id: 'droppable-2',
});
return (
<section>
<div ref={setFirstDroppableRef}>
{/* レンダリング要素が入ります。*/}
</div>
<div ref={setsecondDroppableRef}>
{/* レンダリング要素が入ります。*/}
</div>
</section>
);
}
次に、 useDroppable
フックについて、どんな引数を受け取ることができるのかを紹介します。
useDroppableに指定可能な引数は以下のとおりです。
interface UseDroppableArguments {
id: string | number;
disabled?: boolean;
data?: Record<string, any>;
}
それぞれについて説明をします。
id
:string
型もしくはnumber
型であり、一意の値を設定する必要があります。useDroppable
とuseDraggable
のid
は一致していても構いません。disabled
:boolean
型であり、true
を設定するとドロップを無効化することができます。data
:イベントハンドラやカスタムセンサー内でDroppableコンポーネントに関して、追加のアクセスが必要な際に使われます。
次にプロパティについて解説をします。プロパティは以下のとおりです。
{
rect: React.MutableRefObject<LayoutRect | null>;
isOver: boolean;
node: React.RefObject<HTMLElement>;
over: {id: UniqueIdentifier} | null;
setNodeRef(element: HTMLElement | null): void;
}
setNodeRef
:useDroppableフックを正しく機能させるためには、setNodeRefプロパティが、ドロップ可能な領域にしようとするHTML要素に付与されている必要がありますnode
:`setNodeRef`に渡される現在のノードへの参照rect
:高度な使用例として、ドロップ可能領域の外接矩形の測定が必要な場合isOver
:ドラッグ可能な要素がドロップ可能なコンテナの上にドラッグされたときにtrue
になりますover
:別のドロップ可能なコンテナの上にドラッグされた<Droppable/>
に応じて<Droppable/>
の外観を変更したい場合は、over
に値が定義されているかどうかを確認します。
useDraggableフックについて理解する
useDraggable
フックは、ドラッグ可能な要素を作成するために使用します。 useDraggable
フックを使用するためには、最低限 setNodeRef
関数をDOM要素に渡す必要があります。
useDraggable
フックは以下のように使用します。
import {useDraggable} from '@dnd-kit/core';
import {CSS} from '@dnd-kit/utilities';
function Draggable() {
const {attributes, listeners, setNodeRef, transform} = useDraggable({
id: 'unique-id',
});
const style = {
transform: CSS.Translate.toString(transform),
};
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{/* ドラッグ可能な要素が入る */}
</button>
);
}
次に、 useDraggable
フックがどのような引数を受け取ることができるのかについて紹介します。
interface UseDraggableArguments {
id: string | number;
attributes?: {
role?: string;
roleDescription?: string;
tabIndex?: number;
},
data?: Record<string, any>;
disabled?: boolean;
}
基本的には useDroppable
のものと変わりません。 attributes
については、 role
、 roleDescription
、 tabIndex
を指定することができます。 role
は button
、 roleDescription
はドラッグ可能な要素、 tabIndex
はフォーカスが移動する順番を決定します。
次に、 useDraggable
フックがどのようなプロパティを返すのかについて紹介します。
{
active: {
id: UniqueIdentifier;
node: React.MutableRefObject<HTMLElement>;
rect: ViewRect;
} | null;
attributes: {
role: string;
tabIndex: number;
'aria-diabled': boolean;
'aria-roledescription': string;
'aria-describedby': string;
},
isDragging: boolean;
listeners: Record<SyntheticListenerName, Function> | undefined;
node: React.MutableRefObject<HTMLElement | null>;
over: {id: UniqueIdentifier} | null;
setNodeRef(HTMLElement | null): void;
setActivatorNodeRef(HTMLElement | null): void;
transform: {x: number, y: number, scaleX: number, scaleY: number} | null;
}
ここでは長くなるので説明は省略します。詳しくは公式ドキュメントを参照してみてください。
以上でdnd-kitの構成に関しての説明を終わります。次からは実際にコードを書いていきます。
dnd-kitの使い方
インストール
それでは、はじめにdnd-kitをインストールしていきます。dnd-kitはnpmでインストールすることができます。今回は、 @dnd-kit/core
と、 @dnd-kit/sortable
をインストールします。Reactがインストールされた環境で以下のインストールコマンドを実行します。
npm install @dnd-kit/core @dnd-kit/sortable
以上で、dnd-kitのインストールは完了です。今回インストールした @dnd-kit/core
と @dnd-kit/sortable
の他にも、 @dnd-kit/modifiers
などのパッケージがあります。詳しくは公式ドキュメント
ドラッグ可能な要素を作成する
それではドラッグ可能な要素を作成していきましょう。まずは、 useDraggable
フックを使用してドラッグ可能な要素を作成します。
useDraggable
フックをインポートします。
import {useDraggable} from '@dnd-kit/core';
次に、 useDraggable
を使うための雛形を作成していきます。
import {useDraggable} from '@dnd-kit/core';
export const Draggable = () => {
return (
<div>
{/* ドラッグ可能な要素が入ります */}
</div>
)
}
そして、この Draggable
コンポーネントに useDraggable
フックを使用していきます。
import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
export const Draggable = (props) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: 'unique-id',
});
const style = transform ? {
transform: translate3d(${transform.x}px, ${transform.y}px, 0),
} : undefined;
return (
<div ref={setNodeRef}>
<button style={style} {...listeners} {...attributes}>{props.children}
</button>
</div>
);
}
ここで、 useDraggable
フックで受け取る listeners
は、ドラッグ可能な要素に対して
onMouseDown
onTouchStart
onKeyDown
などのイベントハンドラーを設定するために使用します。
また、 attributes
は、 role
、 tabIndex
、 aria-disabled
、 aria-roledescription
、 aria-describedby
などの属性を設定するために使用します。 transform
はどのくらいドラッグされたのかを取得することができます。
以上でドラッグ可能な要素を作成することができました。次に、ドロップ可能な要素を作成していきます。
ドロップ可能な要素を作成する
ドロップ可能な要素を作成するためには、 useDroppable
フックを使用します。
import {useDroppable} from '@dnd-kit/core';
次に、これらのフックを使うための雛形を作成していきます。
import {useDroppable} from '@dnd-kit/core';
function Droppable() {
const {setNodeRef} = useDroppable({
id: 'unique-id',
});
return (
<div ref={setNodeRef}>
{/* ドロップ可能な要素が入ります */}
</div>
);
}
div
タグの中に入っている要素上にドロップすることが可能です。これだけでは本当に正常に動作しているのかわからないのでドラッグ可能な要素と組み合わせて動作させてみましょう。Reactのプロジェクト内に、 App.jsx
、 Droppable.jsx
、 Draggable.jsx
の3つを作成し以下のようにコードを記述します。
import React, {useState} from 'react';
import {DndContext} from '@dnd-kit/core';
import {Droppable} from './Droppable';
import {Draggable} from './Draggable';
function App() {
const [isDropped, setIsDropped] = useState(false);
const draggableMarkup = (
<Draggable>Drag me</Draggable>
);
return (
<DndContext onDragEnd={handleDragEnd}>
{!isDropped ? draggableMarkup : null}
<Droppable>
{isDropped ? draggableMarkup : 'Drop here'}
</Droppable>
</DndContext>
);
function handleDragEnd(event) {
if (event.over && event.over.id === 'droppable') {
setIsDropped(true);
}
}
}
import React from 'react';
import {useDroppable} from '@dnd-kit/core';
export function Droppable(props) {
const {isOver, setNodeRef} = useDroppable({
id: 'droppable',
});
const style = {
color: isOver ? 'green' : undefined,
};
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
}
import React from 'react';
import {useDraggable} from '@dnd-kit/core';
export function Draggable(props) {
const {attributes, listeners, setNodeRef, transform} = useDraggable({
id: 'draggable',
});
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{props.children}
</button>
);
}
以上を実行することで、ドロップ可能な領域上でのみドロップできてそれ以外ではドロップできないことがわかるかと思います。
実際にdnd-kitを使ってみる
今回は、以下のような縦に並び替えることのできる要素を作成してみます。
では、実際に作成してみましょう。
Reactとdnd-kitをインストールする
create-react-appを使ってReactプロジェクトを作成します。
以下のコマンドを実行します。(フォルダ名などは各自の環境に合わせて修正をしてください。)
$ mkdir dnd-practice
$ cd dnd-practice
$ npx create-react-app .
Reactのインストールが完了したら、dnd-kitをインストールします。
$ npm install @dnd-kit/core @dnd-kit/sortable
自分はスタイルを書くのにScssを利用したのでそれ用のパッケージもインストールしておきます。
$ npm install sass
これで準備は完了しました。次から実際にコードを書いていきます。
実際にコードを書く
要素の作成の前にデフォルトで記述されているコードを削除します。具体的には、 src
ディレクトリの中の index.js
と App.js
をのこしてそれ以外は削除します。また、以下のような構成になるようフォルダ・ファイルを作成します。(自分の環境では、 App.jsx
に拡張子を変更しています。)
src
├── App.jsx
├── components/
├── index.js
└── styles/
└── styles.scss
次に components
ディレクトリ配下に Draggable.jsx
を作成します。そして以下のように記述します。
import '../styles/styles.scss'
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import {CSS} from '@dnd-kit/utilities';
const Draggable = (props) => {
const { attributes, listeners, setNodeRef, transform, transition, } = useSortable({
id: props.num,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<button className="cards" ref={setNodeRef} style={style} {...listeners} {...attributes}>
<p>{props.num}</p>
</button>
);
}
export default Draggable;
props
で表示する内容を num
として受け取ります。 id
属性には一意の値を設定しなければならないので同様に num
を指定します。また、 style
にてドラッグによる移動距離をリアルタイムで反映できるようにしています。
以上の用意ができたら App.jsx
と、スタイルの記述をしていきます。
以下のように styles.scss
に記述します。
body {
background-color: #efefef;
}
.App {
max-width: 580px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
}
.cards {
width: 400px;
height: 40px;
border: 1px solid #eee;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
padding: 20px 10px;
box-sizing: border-box;
margin: 0 auto 20px;
background-color: #fff;
}
App.jsx
には以下のように書きます。
import React, { useState } from 'react';
import { DndContext } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';
import Draggable from './components/Draggable';
import './styles/styles.scss'
const App = () => {
const [items, setItems] = useState(['1', '2', '3', '4']);
return (
<DndContext>
<div className="App">
<SortableContext items={items}>
{items.map((item) => (
<Draggable key={item} num={item} />
))}
</SortableContext>
</div>
</DndContext>
);
}
export default App;
先に述べたように、Draggableな要素を <DndContext>
にて囲んでいます。この時点では、以下のようになっています。
既にそれぞれの要素をドラッグできるかと思います。ここまででもdnd-kitの手軽さが伝わりますでしょうか。
ここからは実際に並び替える機能を実装してまいります。
要素の並び替えを実装する
ここからは要素の並び替え機能を実装していきます。流れとしては、
- 要素をドラッグする
- 要素をドロップする
- ドロップ要素とその時上にある要素を検知する
items
配列を更新する
となります。
実際にコードを用いて解説をしていきます。
dnd-kitにてドラッグイベントを検知するためには <DndContext>
に onDragOver
属性を指定します。この属性は公式ドキュメントに以下のように記載があります。
ドラッグ可能なアイテムがドロップ可能なコンテナの上に移動したときに、そのドロップ可能なコンテナの一意の識別子とともに発火する。
その他にも、ドラッグが終了したときなどに発火するイベントなども設定可能ですので、詳細を知りたい方は公式ドキュメントをご確認ください。
本題に戻ります。
App.jsx
に以下のように追記します。
import React, { useState } from 'react';
import { DndContext } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';
import Draggable from './components/Draggable';
import './styles/styles.scss'
const App = () => {
const [items, setItems] = useState(['1', '2', '3', '4']);
function reorderArray(array, active, over) {
const activeIndex = array.indexOf(active);
const overIndex = array.indexOf(over);
if (activeIndex === -1 || overIndex === -1) {
throw new Error("要素が配列内に存在しません。");
}
const newArray = [...array];
newArray.splice(activeIndex, 1);
newArray.splice(overIndex, 0, active);
return newArray;
}
function handleDragOver(event) {
const { over, active } = event;
if (over && active && over.id !== active.id) {
setItems(prevItems => reorderArray(prevItems, active.id, over.id));
}
}
return (
<DndContext onDragOver={handleDragOver}>
<div className="App">
<SortableContext items={items}>
{items.map((item) => (
<Draggable key={item} num={item} />
))}
</SortableContext>
</div>
</DndContext>
);
}
export default App;
reorderArray
関数を用いて配列の並びかえを実装しています。画像を用いて動きのイメージを示します。
配列 [1, 2, 3, 4]
に対して4番目の要素が2番目の要素の上に重なってドロップされたら2番目の要素の前に4番目の要素を挿入します。
最終的に [1, 4, 2, 3]
という配列を返します。この変更後の配列を useState
を用いて items
配列を更新します。
以上で完了です。実際に並び替えができることが確認できるかと思います。
終わりに
いかがでしたでしょうか。ドラッグアンドドロップを実装することは、アプリなどにおいてユーザーの直感的な操作を実現する手段として非常に強力です。
アプリケーション開発などをしている方はぜひ、dnd-kitを使ってみてください。