はじめに
こんにちは、よわよわエンジニアのshungikuです!
今回はReactでドラッグ&ドロップを実現するためのライブラリである「React DnD」についての入門記事になります。
本家のページにもチュートリアルはありますが、チェスを題材にしていて謎に難しいです。(てかチェスとかよう知らんし!)
それに、自由な位置にドラッグ&ドロップする方法についてはチュートリアルがなかったため、自分が使ってみて得た知見をまとめていきたいと思います。
この記事の対象者
- React DnD を使ってみたいが、重厚長大なチュートリアルしかないためよう分からんという人
- React DnD を使って自由な位置にドラッグ&ドロップする方法を知りたい人
React DnDの使い方
パッケージのインストール
まずは2つのパッケージをインストールしましょう。
npm install react-dnd react-dnd-html5-backend
インストールが完了したら、早速React DnDを使っていきましょう。
DndProviderでラップ
まずは、ドラッグ&ドロップを使いたい範囲をDndProvider
でラップします。
本稿では、Example
というコンポーネントに実装例を書いていくので、そのコンポーネントがあるという体で以下のようにラップします。
ちなみに、backend
というpropsにHTML5Backend
を渡していますが、一旦魔法だと思ってスルーしましょう。
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
ファイルを作ってそこに書いていきましょう。
箱の位置であるtop
とleft
はuseState
でStateとして定義します。
後々、ドロップされた時にsetBox
で位置情報を更新します。
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を使います。
const [collected, drag, dragPreview] = useDrag(
{
type: "box",
item: { top: box.top, left: box.left }
},
[box]
);
引数
第一引数
第一引数には動作仕様を定義するためのオブジェクト、もしくはオブジェクトを返す関数を渡します。例ではオブジェクトを渡しています。
オブジェクトのkeyはいくつか種類があるのですが、ここではまずtype
とitem
の2つだけ紹介します。
type
にはDndProvider
内で一意のIDを記述します。(例ではbox
)
後々説明しますが、ドロップ側でも同じIDを指定することで、ドラッグ側とドロップ側で連携が取れるようになります。
item
にはドラッグ側とドロップ側でやりとりしたい情報を記述します。
例では、box
の top
と left
の情報を渡しています。
第二引数
第二引数には依存関係を持つ変数を配列で渡します。useMemoなどと同様の使い方ですね。
例では、State変数であるbox
に依存していますので、box
を配列に入れています。
これを指定しないと正常に動作しなくなるので、興味のある方は試してみてください。
戻り値
戻り値は配列で3つ返ってきますが、まずは2つ目(例ではdrag
)だけにフォーカスして、他2つは一旦スルーしてください。
2つ目の戻り値をドラッグ対象の要素にref
で渡してあげます。
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を使います。
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はいくつか種類があるのですが、ここではまずaccept
とdrop(item, monitor)
の2つだけ紹介します。
accept
にはuseDrag
で指定したIDと同じものを指定します。(例ではbox
)
これにより、ドラッグ側と連携が取れるようになります。
drop(item, monitor)
にはドロップ時に発火させる関数を指定します。
関数は2つの引数を持ちます。
1つ目(例ではitem
)には、useDrag
のitem
で指定した値が渡ってきます。
2つ目(例ではmonitor
)にはドラッグ&ドロップ中の状態をモニターするためのオブジェクトが渡ってきます。詳細な説明は一旦省きますが、様々な便利メソッドを具備しています。
ここで、drop(item, monitor)
における処理を詳しく見ていきましょう。
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座標差分としてそれぞれ負の値が取得され、右や下に動かしたら正の値が取得されます。
その後、ドラッグ側から渡ってきた初期位置(例では、item
のleft
とtop
)に対して取得した差分を加算することで、ドロップ後の位置を算出し、setBox
でState変数を更新します。
これにより、State変数のtop
とleft
の値が更新され、再描画の処理が走り、対象要素がドロップ後の位置に描画されます。
ちなみに、drop(item, monitor)
の戻り値にはundefined
または何らかのオブジェクトを指定します。
オブジェクトを返すときのユースケースについては、また別の記事で紹介したいと思います。
第二引数
第二引数には依存関係を持つ変数を配列で渡します。
例では、特に依存変数はないため空配列を渡しています。
戻り値
戻り値は配列で2つ返ってきますが、まずは2つ目(例ではdrop
)だけにフォーカスして、他は一旦スルーしてください。
2つ目の戻り値をドロップ対象の要素にrefで渡してあげます。
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>
)
完成物
ここまでの全体のコードを掲載します。
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>
);
}
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の使い方を説明してみました。
取っ掛かりとして参考になれば幸いです。
また、"超入門"ということで出来るだけシンプルな内容に留めたかったため、いろいろな箇所の説明を端折っています。
より詳細な使い方については、気が向いたらまた別記事で書いていけたらと思っています。