【dnd-kit】Reactでドラッグアンドドロップ機能を実装するモダンなライブラリdnd-kitの使い方を解説

カテゴリー
公開日 最終更新日
【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>;
}

それぞれについて説明をします。

  • idstring型もしくは number型であり、一意の値を設定する必要があります。 useDroppableuseDraggableidは一致していても構いません。
  • disabledboolean型であり、 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については、 roleroleDescriptiontabIndexを指定することができます。 rolebuttonroleDescriptionはドラッグ可能な要素、 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は、 roletabIndexaria-disabledaria-roledescriptionaria-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.jsxDroppable.jsxDraggable.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.jsApp.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の手軽さが伝わりますでしょうか。

ここからは実際に並び替える機能を実装してまいります。

要素の並び替えを実装する

ここからは要素の並び替え機能を実装していきます。流れとしては、

  1. 要素をドラッグする
  2. 要素をドロップする
  3. ドロップ要素とその時上にある要素を検知する
  4. 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を使ってみてください。