サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Create React App + React DnD 02: ドラッグ&ドロップで動かす


React DnDサイトの「Tutorial」でつくられるドラッグ&ドロップのサンプルコードに、少し手を加えて解説し直すチュートリアルの第2回です。いよいよ、ドラッグしてみます。

01 コンポーネントをドラッグする

まず、React DnDをインストールしなければなりません。React DnDは、HTMLドラッグ&ドロップAPIにもとづいてマウス操作を扱います。そのために必要になるのが、バックエンドと呼ばれるプラグインです。そこで、react-dnd-html5-backendもインストールに加えてください。


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

なお、このバックエンドはタッチスクリーンでは使えません。タッチ操作には、バックエンドとしてreact-dnd-touch-backendを用います。

アプリケーションにReact DnDの機能を与えるのが、DndProviderコンポーネントです。バックエンド(HTML5Backend)は、backendプロパティに定めてください。クリックイベントハンドラは、もう外してしまいます。

src/components/Board.js

import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'
// import { canMoveKnight, moveKnight } from '../Game';

function renderSquare(i, [knightX, knightY]) {

	return (
		<div
			key={i}
			style={squareStyle}
			// onClick={(() => handleSquareClick(x, y))}
		>

		</div>
	);
}
/* function handleSquareClick(toX, toY) {
	if (canMoveKnight(toX, toY)) {
		moveKnight(toX, toY);
	}
} */

function Board({ knightPosition }) {

	return (
		<DndProvider backend={HTML5Backend}>
			<div style={boardStyle}>
				{squares}
			</div>
		</DndProvider>
	);
}

React DnDがドラッグする対象は、コンポーネントでもDOMノードでもなく、itemと呼ばれるオブジェクトつまりデータです。itemは識別のためのtypeというプロパティを必ず備えます。ほかに、ドラッグ対象についての情報をプロパティに含めても構いません(ただし、あまり詰め込みすぎないように)。typeの値は文字列(またはsymbol)で、型決めのモジュールに定数として定めることがお勧めです(コード001)。

コード001■型を定数として定めるモジュール

src/ItemTypes.js

export const ItemTypes = { 
	KNIGHT: 'knight', 
} 

ここでReact DnDのuseDragフック(hook)を使います。引数はコールバック関数で、返されるのがドラッグ操作に必要な仕様を収めたオブジェクトです。プロパティにはtypecollect()関数を加えました。typeは、上述のitemオブジェクトを識別するプロパティです。関数collect()は、引数のmonitorからドラッグするオブジェクトの状態として用いるプロパティを取り出し、オブジェクトにして返します。

useDragフッククは配列を返します。はじめの要素はcollect()関数の集めたプロパティが収められたオブジェクトです。monitorオブジェクトのisDragging()メソッドは、ドラッグ中かどうかを論理値で返します(ノート01)。ふたつ目の要素(drag)がドラッグしているオブジェクトとDOMを結ぶconnector()関数です。この関数は、ドラッグするJSX要素のref属性に与えてください。

src/components/Knight.js

import { useDrag } from 'react-dnd';
import { ItemTypes } from '../ItemTypes';

const Knight = () => {
	const [{ isDragging }, drag] = useDrag(() => ({ 
		type: ItemTypes.KNIGHT,
		collect: (monitor) => ({
			isDragging: !!monitor.isDragging(),
		}),
	}));  
	return (
		// <div style={knightStyle}>
		<div
			ref={drag}
			style={{
				...knightStyle,
				opacity: isDragging ? 0.5 : 1,
			}}
		>
			♘
		</div>
	)
};

ノート01■二重の論理否定演算子

論理否定演算子!をふたつ添えると、truetruefalsefalseのまま変わりません。けれど、オペランド(被演算子)がブール(論理)値でないとき、ひとつめの演算子でブール値評価されたうえで否定(反転)されます。つまり、結果を必ずブール値にすることが目的です。

これでドラッグはできるものの、ドロップできません(図001)。マウスボタンを放せば、もとの位置に戻ってしまいます。つぎにドロップ先を決めなければならないのです。useDragフックについては、つぎに簡単にまとめておきます。引数などは、今回扱う範囲でご紹介しました。詳しくは、公式サイトのuseDragをお読みください。

構文001■useDragフック

useDrag
引数
  1. ドラッグの仕様が収められたオブジェクトを返すコールバック関数。または、仕様オブジェクトを直に渡してもよい。
    • type: ドロップ先を識別する文字列またはシンボル(必須)。
    • item: ドラッグする項目の情報を示すオブジェクトまたはそれを返す関数(必須)。
    • collect: コンポーネントに注入されるドラッグプロパティのオブジェクトを返す関数。
      • monitor: collect関数が受け取る引数。ドラッグされているコンポーネントの状態を監視するオブジェクト。
    • 他のプロパティを加えることもできる。
  2. フックの戻り値をメモ化するための依存配列(メモ化と依存配列については、「Create React App 入門 08: useMemoフックで無駄な再計算を省く」参照)。デフォルト値はつぎのとおり。
    • 引数が関数の場合は、空の配列[]
    • 引数がオブジェクトの場合は、仕様オブジェクトに収めたプロパティの配列。
戻り値 ドラッグの内容を表す配列。
  1. collect()関数が集めたプロパティを収めるオブジェクト。
  2. ドラッグソースのオブジェクトとDOMを結ぶconnector()関数
  3. ドラッグのプレビュー表示とDOMを結ぶconnector()関数

図001■ドラッグできてもドロップするともとの位置に戻ってしまう

図001

02 コンポーネントをドロップする

ナイトを動かすときのマス目の位置情報は、これまで盤面のコンポーネント(Board)が担ってきました。ドラッグ&ドロップを扱おうとすると、コードが込み入ってきます。そこで、盤面から位置情報にもとづく処理を切り分けましょう。

もっとも、位置情報の処理をマス目(Square)に移したのでは、切り分けたことになりません。マス目を包む新たな位置情報のコンポーネント(BoardSquare)を、このあとつくることにします。

src/components/Board.js

// import Square from './Square';
import BoardSquare from './BoardSquare';

function renderSquare(i, [knightX, knightY]) {

	// const black = (x + y) % 2 === 1;
	// const isKnightHere = knightX === x && knightY === y;
	// const piece = isKnightHere ? <Knight /> : null;
	return (
		<div key={i} style={squareStyle}>
			{/* <Square black={black}>{piece}</Square> */}
			<BoardSquare x={x} y={y}>
				{renderPiece(x, y, [knightX, knightY])}
			</BoardSquare>
		</div>
	);
}
function renderPiece(x, y, [knightX, knightY]) {
	const isKnightHere = knightX === x && knightY === y;
	return isKnightHere ? <Knight /> : null;
}

ドロップの動きを決めるのは、React DnDのuseDropフックです。引数はコールバック関数で、ドロップ操作に必要な仕様が収められたオブジェクトを返します。プロパティacceptの値は、ドロップできるitemオブジェクトのtypeです。そして、受け入れ可能な項目がドロップされると、仕様オブジェクトに定めたdrop()メソッドが呼び出されます。フックの戻り値は配列で、ふたつ目の要素はドロップ先オブジェクトとDOMを結ぶconnector()関数です。ドロップするJSX要素のref属性に与えてください。

新たなモジュールsrc/components/BoardSquare.jsの定めは、つぎのとおりです。これで、ナイトがドラッグ&ドロップできるようになります。ただし、ドロップ先が適切かどうかは判定していません。そのため、どのマス目にもドロップできます(図002)。

src/components/BoardSquare.js

import React from 'react';
import { useDrop } from 'react-dnd';
import { moveKnight } from '../Game';
import { ItemTypes } from '../ItemTypes';
import Square from './Square';

const boardSquareStyle = {
	position: 'relative',
	width: '100%',
	height: '100%',
};
const BoardSquare = ({ x, y, children }) => {
	const black = (x + y) % 2 === 1;
	const [, drop] = useDrop(() => ({  
		accept: ItemTypes.KNIGHT,
		drop: () => moveKnight(x, y),
	}), [x, y]);  
	return (
		<div
			ref={drop}
			style={boardSquareStyle}
		>
			<Square black={black}>{children}</Square>
		</div>
	);
};

export default BoardSquare;

この機会に、ナイトの駒のスタイルも少し整えましょう。

src/components/Knight.js

const knightStyle = {
	fontSize: 60,  /* 40, */

	textAlign: 'center',
	lineHeight: '4rem',

};

図002■盤面のどのマス目にもドロップできる

図002

useDropフックについて、つぎに簡単にまとめておきます。引数などは、今回扱う範囲でご紹介しました。詳しくは、公式サイトのuseDropをお読みください。盤面のモジュールのスクリプト全体は、以下のコード002のとおりです。

構文002■useDropフック

useDrop
引数
  1. ドロップの仕様が収められたオブジェクトを返すコールバック関数。または、仕様オブジェクトを直に渡してもよい。
    • accept: ドロップ可能なソースオブジェクトのtypeを示す値(必須)。
    • drop: 受け入れ可能な項目がドロップされたときに呼び出される関数。
      • item: 関数が受け取る第1引数。ドラッグされている項目オブジェクト。
      • monitor: 関数が受け取る第2引数。ドラッグされているコンポーネントの状態を監視するオブジェクト。
    • canDrop: ドラッグした項目がドロップ先ターゲットに受け入れられるかどうかを返す関数。
      • item: 関数が受け取る第1引数。ドラッグされている項目オブジェクト。
      • monitor: 関数が受け取る第2引数。ドラッグされているコンポーネントの状態を監視するオブジェクト。
    • collect: コンポーネントに注入されるドロッププロパティのオブジェクトを返す関数。
      • monitor: collect関数が受け取る引数。ドラッグされているコンポーネントの状態を監視するオブジェクト。
  2. フックの戻り値をメモ化するための依存配列(メモ化と依存配列については、「Create React App 入門 08: useMemoフックで無駄な再計算を省く」参照)。デフォルト値はつぎのとおり。
    • 引数が関数の場合は、空の配列[]
    • 引数がオブジェクトの場合は、仕様オブジェクトに収めたプロパティの配列。
戻り値 ドラッグの内容を表す配列。
  1. collect()関数が集めたプロパティを収めるオブジェクト。
  2. ドラッグターゲットのオブジェクトとDOMを結ぶconnector()関数
  3. ドラッグのプレビュー表示とDOMを結ぶconnector()関数

コード002■盤面のモジュール

src/components/Board.js

import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'
import BoardSquare from './BoardSquare';
import Knight from './Knight';

const boardStyle = {
	width: 500,
	height: 500,
	border: '1px solid gray',
	display: 'flex',
	flexWrap: 'wrap'
};
const squareStyle = { width: '12.5%', height: '12.5%'};
function renderSquare(i, [knightX, knightY]) {
	const x = i % 8;
	const y = Math.floor(i / 8);
	return (
		<div
			key={i}
			style={squareStyle}
		>
			<BoardSquare x={x} y={y}>
				{renderPiece(x, y, [knightX, knightY])}
			</BoardSquare>
		</div>
	);
}
function renderPiece(x, y, [knightX, knightY]) {
	const isKnightHere = knightX === x && knightY === y;
	return isKnightHere ? <Knight /> : null;
}
function Board({ knightPosition }) {
	const squares = [];
	for (let i = 0; i < 64; i++) {
		squares.push(renderSquare(i, knightPosition));
	}
	return (
		<DndProvider backend={HTML5Backend}>
			<div style={boardStyle}>
				{squares}
			</div>
		</DndProvider>
	);
}

export default Board;

03 コンポーネントにオーバーしたときの処理

useDropフックが返す配列のはじめの要素は、collect()関数により集められたプロパティのオブジェクトです。関数の引数オブジェクトに、コールバックcollect()として定めてください。コールバックが受け取るmonitorisOver()メソッドを呼び出すと、ドロップ先に重なっているかの論理値が得られます。

モジュールsrc/components/BoardSquare.jsをつぎのように書き替えれば、ドラッグで重ねたマス目が半透明の黄色になります。

src/components/BoardSquare.js

const overlayStyle = {
	position: 'absolute',
	top: 0,
	left: 0,
	height: '100%',
	width: '100%',
	zIndex: 1,
	opacity: 0.5,
	backgroundColor: 'yellow',
}
const BoardSquare = ({ x, y, children }) => {

	// const [, drop] = useDrop(() => ({
	const [{ isOver }, drop] = useDrop(() => ({

		collect: monitor => ({
			isOver: !!monitor.isOver(),
		}),
	}), [x, y]);  
	return (
		<div
			ref={drop}
			style={boardSquareStyle}
		>

			{isOver && (<div style={overlayStyle} />)}
		</div>
	)
}

04 ドロップできるマス目かどうか色分けする

ドラッグしているときのマス目の色を、3種類にしましょう。まず、ナイトをドロップできるマス目は黄色にします。オーバーしたマス目は2種類に分け、ドロップできなければ赤、できるなら青です。

マス目に重ねて色を変えるコンポーネントは、つぎのように新たなモジュール(src/components/Overlay.js)に分けます(コード003)。引数に受け取るプロパティ(color)が背景色(backgroundColor)に与える色です。

コード003■マス目に重ねて色を変えるモジュール

src/components/Overlay.js

const overlayStyle = {
	position: 'absolute',
	top: 0,
	left: 0,
	height: '100%',
	width: '100%',
	zIndex: 1,
	opacity: 0.5,
};
const Overlay = ({ color }) => {
	return (
		<div
			style={{
				...overlayStyle,
				backgroundColor: color,
			}}
		/>
	);
};
export default Overlay;

モジュールsrc/components/BoardSquare.jsは、マス目に色づけする要素を今つくったコンポーネントと差し替えます。また、Gameモジュールからimportに加えるのが、ドロップしてよいマス目を調べる関数(canMoveKnight())です。また、useDropcollect()で、プロパティcanDropmonitorcanDrop()が返す値を与えてください。すると、仕様オブジェクトにコールバックcanDrop()で定めた戻り値が、ドロップしてよいマス目かどうかを決める論理値として、useDropの戻り値から取り出せるのです。これで、マス目のカラーが3種類に塗り分けされます(図003)。

src/components/BoardSquare.js

// import { moveKnight } from '../Game';
import { canMoveKnight, moveKnight } from '../Game';

import Overlay from './Overlay';

/* const overlayStyle = {
	position: 'absolute',
	top: 0,
	left: 0,
	height: '100%',
	width: '100%',
	zIndex: 1,
	opacity: 0.5,
	backgroundColor: 'yellow',
}; */
const BoardSquare = ({ x, y, children }) => {

	// const [{ isOver }, drop] = useDrop(() => ({
	const [{ isOver, canDrop }, drop] = useDrop(() => ({

		canDrop: () => canMoveKnight(x, y),
		collect: monitor => ({

			canDrop: !!monitor.canDrop(),
		}),
	}), [x, y]);  
	return (
		<div
			ref={drop}
			style={boardSquareStyle}
		>
			<Square black={black}>{children}</Square>
			{/* {isOver && (<div style={overlayStyle} />)} */}
			{isOver && !canDrop && <Overlay color="red" />}
			{!isOver && canDrop && <Overlay color="yellow" />}
			{isOver && canDrop && <Overlay color="green" />}
		</div>
	);
};

図003■ドロップできるマス目は黄色でドロップできないマス目にオーバーすると赤

図003

ノート02■モジュールsrc/components/Overlay.js

「Tutorial」には、モジュールsrc/components/Overlay.jsの記述がありません。けれど、src/components/BoardSquare.jsのJSXにはOverlayコンポーネントが差し込まれているので、記載漏れだと思われます(「Make the Board Squares Droppable」参照)。

モジュールsrc/components/BoardSquare.jsの記述全体は、つぎのコード004にまとめたとおりです。

コード004■ナイトの位置とマス目の処理をするモジュール

src/components/BoardSquare.js

import { useDrop } from 'react-dnd';
import { canMoveKnight, moveKnight } from '../Game';
import { ItemTypes } from '../ItemTypes';
import Square from './Square';
import Overlay from './Overlay';

const boardSquareStyle = {
	position: 'relative',
	width: '100%',
	height: '100%',
};
const BoardSquare = ({ x, y, children }) => {
	const black = (x + y) % 2 === 1;
	const [{ isOver, canDrop }, drop] = useDrop(() => ({
		accept: ItemTypes.KNIGHT,
		drop: () => moveKnight(x, y),
		canDrop: () => canMoveKnight(x, y),
		collect: (monitor) => ({
			isOver: !!monitor.isOver(),
			canDrop: !!monitor.canDrop(),
		}),
	}), [x, y]);  
	return (
		<div
			ref={drop}
			style={boardSquareStyle}
		>
			<Square black={black}>{children}</Square>
			{isOver && !canDrop && <Overlay color="red" />}
			{!isOver && canDrop && <Overlay color="yellow" />}
			{isOver && canDrop && <Overlay color="green" />}
		</div>
	);
};

export default BoardSquare;

05 ドラッグしているときのイメージを変える

useDragが返す配列の3つ目の要素は、ドラッグのプレビュー表示とDOMを結ぶconnector()関数です。React DnDのユーティリティコンポーネントDragPreviewImageと組み合わせて、ドラッグしているときのイメージが定められます。イメージのSVGデータは、公式作例(knightImage.js)からコピーしてsrc/components/knightImage.jsとして保存してください。

モジュールsrc/components/Knight.jsのテンプレートにDragPreviewImageコンポーネントを差し込み、useDragの戻り値から取り出した3つ目の要素(preview)の関数をコンポーネントのconnectに与えます。また、SVGデータのパスはsrcに定めてください。

src/components/Knight.js

// import { useDrag } from 'react-dnd';
import { DragPreviewImage, useDrag } from 'react-dnd';
import { knightImage } from './knightImage';

const Knight = () => {
	// const [{ isDragging }, drag] = useDrag(() => ({
	const [{ isDragging }, drag, preview] = useDrag(() => ({

		}),
	}));  
	return (
		<>
			<DragPreviewImage connect={preview} src={knightImage} />
			<div

			>

			</div>
		</>
	);
};

これで、ナイトの駒をドラッグすると、その間のイメージが変わります(図004)。書き替えたナイトのモジュールのスクリプト全体は、以下のコード005のとおりです。なお、モジュールsrc/Game.jssrc/components/Square.jsのコードは、前回のまま手は加えていません。React DnDサイトの「Tutorial」作例ができ上がりました。CodeSandboxにはサンプル001を公開しましたので、各モジュールのコードと動きはこちらでお確かめください。

もっとも、公式「Tutorial」が公開している作例を見ると、コードがあちこち異なります。それどころか、この公開作例には「Tutorial」に解説されていないモジュールさえ含まれています。そこで次回は、今回書き上げたコードを公開作例に合わせて手直ししてみます。

図004■ドラッグ中のイメージが変わる

図004

コード005■ドラッグ中のイメージが変わるナイトのモジュール

src/components/Knight.js

import { DragPreviewImage, useDrag } from 'react-dnd';
import { knightImage } from './knightImage';
import { ItemTypes } from '../ItemTypes';

const knightStyle = {
	fontSize: 60,
	fontWeight: 'bold',
	textAlign: 'center',
	lineHeight: '4rem',
	cursor: 'move',
};
const Knight = () => {
	const [{ isDragging }, drag, preview] = useDrag(() => ({
		type: ItemTypes.KNIGHT,
		collect: (monitor) => ({
			isDragging: !!monitor.isDragging(),
		}),
	}));  
	return (
		<>
			<DragPreviewImage connect={preview} src={knightImage} />
			<div
				ref={drag}
				style={{
					...knightStyle,
					opacity: isDragging ? 0.5 : 1,
				}}
			>
				♘
			</div>
		</>
	);
};

export default Knight;

サンプル001■React DnD 02: Chess board and lonely Knight

Create React App + React DnD


作成者: 野中文雄
更新日: 2021年06月08日 構文002に加筆・補正。
更新日; 2021年05月01日 本文の説明を一部追加・補正。
更新日: 2021年04月22日 React DnDの最新バージョンに合わせたコードと解説の改訂。
作成日: 2020年04月05日


Copyright © 2001-2020 Fumio Nonaka.  All rights reserved.