0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

dnd-kitで1つのコンテナ内で要素を縦に並び替え

Last updated at Posted at 2025-03-31

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つのコンテナ内で複数のアイテムを縦に動かせる機能を実装します。

demo.gif

かなりシンプルですが、ディレクトリ構造は下記になります。
(App routerを使用したので、app配下に作成していきます)

src/app/
 ├ page.tsx
 └ component
  └ DragItem.tsx

1. コアライブラリのインストール

まず下記コマンドで自身のReactプロジェクトにdnd-kitのコアライブラリをインストールします。

npm install @dnd-kit/core

2. DndContextを用意する

DndContextはアプリケーション内のドラッグ&ドロップの状態を管理し、必要な情報を子コンポーネントに渡します。
つまりドラッグ ・ドロップ可能要素は全てDndContextに囲まれている必要があるので、
まずはこちらを用意します。

page.tsx
'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の配下に追加します。

page.tsx
'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で定義しておきます。

定義したitemsSortableContextitemsに渡しておきます。

page.tsx
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の追加

並び替え対象の要素に使用するフックです。

DragItem.tsx
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で用意しておいたidtitleを受け取るようにします。

page.tsx
  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の関数名にし、下記のように定義しました。

page.tsx
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へ渡します。

page.tsx
{/* 中略 */}

return (
    <>
      <DndContext onDragEnd={itemDragEnd}>

9. 完成

これで各要素をドラッグ & ドロップができるようになっていると思います。

ドラッグ要素にonClick属性をつけたい

そのままcomponentにonClickを追加してもうまく動きません。
この場合はsensorsを使用して、「何px動かしたらドラッグを判定するか」を定義します。

まず@dnd-kit/coreからuseSensoruseSensorsMouseSensorをimportします。

page.tsx
import { DndContext, DragEndEvent, useSensor, useSensors, MouseSensor } from '@dnd-kit/core';

importしたsensor達は下記のように記述します。
これでマウスにより対象要素が5px(= distanceに設定した数値)以上動いた場合 = ドラッグされたと認識するようになります。

page.tsx
  const sensors = useSensors(
    useSensor(MouseSensor, { activationConstraint: { distance: 5 } })
  )

作成したsensorsDndContextに渡します。
これでclickイベントにも反応してくれるようになります。

page.tsx
  return (
    <>
      <DndContext onDragEnd={itemDragEnd} sensors={sensors}>

ドラッグ中のみ該当要素のstyleを変えたい

ドラッグ中であることが分かりやすいように見た目を変えることも可能です。
調べてみるとDragOverlayなども紹介されていましたが、今回はドラッグ中の要素は背景色を変えたいだけなのでシンプルに実装しました。

DragItemuseSortableからisDraggingを追加で受け取ります。
名前の通り、ドラッグ可能要素がドラッグされているとisDraggingtrueを返します。

DragItem.tsx
  const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id });

isDragingでstyleを分岐させます。
これでドラッグ中の要素のみ背景色を変更することができます。

DragItem.tsx
  const style = transform ? {
    transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
    backgroundColor: isDragging ? '#ccc' : '#fff',
    zIndex: isDragging ? 100 : 1,
  } : undefined;

余談ですが、背景色を変えたところ、ドラッグ中の要素が他の要素の下に回り込んでしまうことに気がついたので、ドラッグ中の要素が常に上に来るようz-indexも追加で指定しました。

要素の該当部分をドラッグした時のみ動かしたい

ドラッグ要素にハンドルを用意し、そこをドラッグした際のみ要素が動かせるようにします。
DragItem.tsxuseSortableからsetActivatorNodeRefを受け取ります。

DragItem.tsx
const { attributes, listeners, setNodeRef, transform, isDragging, setActivatorNodeRef } = useSortable({ id });

今回はDragHandlecomponentを作成したので、こちらをドラッグした時のみ反応するようにします。(DragHandleの中身はハンドルの見た目のみです)
そのラッパー要素に先ほどのsetActivatorNodeRefと、元々外側のdivにあった{...listeners}{...attributes}をハンドル側に移動します。

DragItem.tsx
return (
    <div ref={setNodeRef} style={style} onClick={onClick}>
		 {title}
        <div ref={setActivatorNodeRef} {...listeners} {...attributes} >
          <DragHandle />
        </div>
     </div>
);

{...listeners}だけを移動しても動きますが、公式ドキュメント(こちら)に記載があるように、{...listeners}がアタッチされている同じDOMにも、{...attributes}をアタッチする必要があるようです。

ちなみに上記で紹介した方法を使って、ドラッグ中はbackground-colorを変更して、各アイテムにハンドルを用意した際の下記のような挙動になります。
ハンドル以外をドラッグした際はアイテムが動かず、右のドット箇所のみドラッグ可能になります。
demo.gif

まとめ・感想

今回初めてdnd-kitを使用してみましたが、思っていたよりもシンプルにドラッグ&ドロップの機能自体は実装することができました。
今回は復習として、1つのコンテナ内で要素を縦に動かすのみの挙動でしたが、実際に自己学習で作成した機能は複数コンテナ間での要素を動かす挙動なので、こちらもまたQiitaにまとめたいと思います。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?