12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React DnD 超入門 〜Drag Around 編〜

Posted at

はじめに

こんにちは、よわよわエンジニアのshungikuです!

今回はReactでドラッグ&ドロップを実現するためのライブラリである「React DnD」についての入門記事になります。

本家のページにもチュートリアルはありますが、チェスを題材にしていて謎に難しいです。(てかチェスとかよう知らんし!)
それに、自由な位置にドラッグ&ドロップする方法についてはチュートリアルがなかったため、自分が使ってみて得た知見をまとめていきたいと思います。

drag.gif

この記事の対象者

  • React DnD を使ってみたいが、重厚長大なチュートリアルしかないためよう分からんという人
  • React DnD を使って自由な位置にドラッグ&ドロップする方法を知りたい人

React DnDの使い方

パッケージのインストール

まずは2つのパッケージをインストールしましょう。

npm install react-dnd react-dnd-html5-backend

インストールが完了したら、早速React DnDを使っていきましょう。

DndProviderでラップ

まずは、ドラッグ&ドロップを使いたい範囲をDndProviderでラップします。
本稿では、Exampleというコンポーネントに実装例を書いていくので、そのコンポーネントがあるという体で以下のようにラップします。

ちなみに、backend というpropsにHTML5Backendを渡していますが、一旦魔法だと思ってスルーしましょう。

App.tsx
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Example } from "./Example";

export default function App() {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>
        <Example />
      </DndProvider>
    </div>
  );
}

見た目の作成

続いて、ドラッグ対象の箱と、ドロップ可能範囲の枠を作ります。
Example.tsxファイルを作ってそこに書いていきましょう。

箱の位置であるtopleftuseStateでStateとして定義します。
後々、ドロップされた時にsetBoxで位置情報を更新します。

Example.tsx
import React from "react";

const ContainerStyle: React.CSSProperties = {
  width: 500,
  height: 500,
  backgroundColor: "silver"
};

const BoxStyle: React.CSSProperties = {
  position: "absolute",
  border: "1px dashed gray",
  backgroundColor: "white",
  padding: "0.5rem 1rem",
  cursor: "move"
};

type Box = {
  top: number;
  left: number;
};

export const Example: React.FC = () => {
  const [box, setBox] = React.useState<Box>({ top: 20, left: 20 });
  return (
    <div style={ContainerStyle}>
      <div style={{ ...BoxStyle, top: box.top, left: box.left }}>
        Drag me around
      </div>
    </div>
  );
};

こんな感じの箱と枠ができます。

ドラッグ制御の実装 ~useDrag~

ドラッグするコンポーネントに対して、useDragというhooksを使います。

Example.tsx
  const [collected, drag, dragPreview] = useDrag(
    {
      type: "box",
      item: { top: box.top, left: box.left }
    },
    [box]
  );

引数

第一引数

第一引数には動作仕様を定義するためのオブジェクト、もしくはオブジェクトを返す関数を渡します。例ではオブジェクトを渡しています。
オブジェクトのkeyはいくつか種類があるのですが、ここではまずtypeitemの2つだけ紹介します。

typeにはDndProvider内で一意のIDを記述します。(例ではbox
後々説明しますが、ドロップ側でも同じIDを指定することで、ドラッグ側とドロップ側で連携が取れるようになります。

itemにはドラッグ側とドロップ側でやりとりしたい情報を記述します。
例では、boxtopleft の情報を渡しています。

第二引数

第二引数には依存関係を持つ変数を配列で渡します。useMemoなどと同様の使い方ですね。
例では、State変数であるboxに依存していますので、boxを配列に入れています。
これを指定しないと正常に動作しなくなるので、興味のある方は試してみてください。

戻り値

戻り値は配列で3つ返ってきますが、まずは2つ目(例ではdrag)だけにフォーカスして、他2つは一旦スルーしてください。

2つ目の戻り値をドラッグ対象の要素にrefで渡してあげます。

Example.tsx
  return (
    <div className='w-[500px] h-[500px] bg-gray-200'>
      {/* ↓ refで渡す部分を追加 ↓ */}
      <div ref={drag} style={{...boxStyle, top: box.top, left: box.left}}>hello</div>
    </div>
  )

ドロップ制御の実装 ~useDrop~

ドロップするコンポーネントに対しては、useDropというhooksを使います。

Example.tsx
  const [collectedProps, drop] = useDrop(
    () => ({
      accept: "box",
      drop(item: Box, monitor) {
        const delta = monitor.getDifferenceFromInitialOffset() as XYCoord;
        const left = Math.round(item.left + delta.x);
        const top = Math.round(item.top + delta.y);
        setBox({ top, left });
        return undefined;
      }
    }),
    []
  );

引数

第一引数

第一引数には動作仕様を定義するためのオブジェクト、もしくはオブジェクトを返す関数を渡します。例ではオブジェクトを渡しています。
オブジェクトのkeyはいくつか種類があるのですが、ここではまずacceptdrop(item, monitor)の2つだけ紹介します。

acceptにはuseDragで指定したIDと同じものを指定します。(例ではbox
これにより、ドラッグ側と連携が取れるようになります。

drop(item, monitor)にはドロップ時に発火させる関数を指定します。
関数は2つの引数を持ちます。

1つ目(例ではitem)には、useDragitemで指定した値が渡ってきます。

2つ目(例ではmonitor)にはドラッグ&ドロップ中の状態をモニターするためのオブジェクトが渡ってきます。詳細な説明は一旦省きますが、様々な便利メソッドを具備しています。

ここで、drop(item, monitor)における処理を詳しく見ていきましょう。

Example.tsx
      drop(item: Box, monitor) {
        const delta = monitor.getDifferenceFromInitialOffset() as XYCoord;
        const left = Math.round(item.left + delta.x);
        const top = Math.round(item.top + delta.y);
        setBox({ top, left });
        return undefined;
      }

例では、monitor.getDifferenceFromInitialOffset()を叩くことで、ドラッグ前と後の位置情報の差分を取得しています。
例えば、左や上に動かしたら x,y座標差分としてそれぞれ負の値が取得され、右や下に動かしたら正の値が取得されます。

その後、ドラッグ側から渡ってきた初期位置(例では、itemlefttop)に対して取得した差分を加算することで、ドロップ後の位置を算出し、setBoxでState変数を更新します。

これにより、State変数のtopleftの値が更新され、再描画の処理が走り、対象要素がドロップ後の位置に描画されます。

ちなみに、drop(item, monitor)の戻り値にはundefinedまたは何らかのオブジェクトを指定します。
オブジェクトを返すときのユースケースについては、また別の記事で紹介したいと思います。

第二引数

第二引数には依存関係を持つ変数を配列で渡します。
例では、特に依存変数はないため空配列を渡しています。

戻り値

戻り値は配列で2つ返ってきますが、まずは2つ目(例ではdrop)だけにフォーカスして、他は一旦スルーしてください。

2つ目の戻り値をドロップ対象の要素にrefで渡してあげます。

Example.tsx
  return (
    <div ref={drop} className='w-[500px] h-[500px] bg-gray-200'>
      <div ref={drag} style={{...boxStyle, top: box.top, left: box.left}}>hello</div>
    </div>
  )

完成物

ここまでの全体のコードを掲載します。

App.tsx
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Example } from "./Example";

export default function App() {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>
        <Example />
      </DndProvider>
    </div>
  );
}
Example.tsx
import React from "react";
import { useDrag, useDrop, XYCoord } from "react-dnd";

const ContainerStyle: React.CSSProperties = {
  width: 500,
  height: 500,
  backgroundColor: "silver"
};

const BoxStyle: React.CSSProperties = {
  position: "absolute",
  border: "1px dashed gray",
  backgroundColor: "white",
  padding: "0.5rem 1rem",
  cursor: "move"
};

type Box = {
  top: number;
  left: number;
};

export const Example: React.FC = () => {
  const [box, setBox] = React.useState<Box>({ top: 20, left: 20 });

  const [collected, drag, dragPreview] = useDrag(
    {
      type: "box",
      item: { top: box.top, left: box.left }
    },
    [box]
  );

  const [collectedProps, drop] = useDrop(
    () => ({
      accept: "box",
      drop(item: Box, monitor) {
        const delta = monitor.getDifferenceFromInitialOffset() as XYCoord;
        const left = Math.round(item.left + delta.x);
        const top = Math.round(item.top + delta.y);
        setBox({ top, left });
        return undefined;
      }
    }),
    []
  );

  return (
    <div ref={drop} style={ContainerStyle}>
      <div ref={drag} style={{ ...BoxStyle, top: box.top, left: box.left }}>
        Drag me around
      </div>
    </div>
  );
};

おわりに

出来るだけ軽量なサンプルコードでReact DnDの使い方を説明してみました。
取っ掛かりとして参考になれば幸いです。

また、"超入門"ということで出来るだけシンプルな内容に留めたかったため、いろいろな箇所の説明を端折っています。
より詳細な使い方については、気が向いたらまた別記事で書いていけたらと思っています。

参考

12
6
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
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?