サイトトップ

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

HTML5テクニカルノート

Create React App + React DnD 03: React DnD公式サイトの作例に書き替える ー 副作用フックuseEffect()を使って


前回は、React DnDサイトの「Tutorial」でつくられるドラッグ&ドロップのサンプルコードを、少し手直ししてつくりました。この「Tutorial」には、でき上がりと称するCodeSandboxの作例も公開されています。でも、コードを見ると、解説と微妙に違っているようです。それどころか、文中にひとことも触れられていないモジュール(src/components/example.js)まで加わっています。そこで今回は、前回のコードにさらに手を加えて、この公式サイトの作例に書き替えてみましょう。ただし、前回に引き続き、細かいところは手直ししました。

01 useEffect()を使う

前回つくった作例「React DnD 02: Chess board and lonely Knight」で気になるのは、おおもとのアプリケーションモジュールsrc\index.jsReactDOM.render()メソッドを呼び出すコードです。関数observe()に渡したコールバック関数は、ナイトの駒の位置(knightPosition)が変わるたびに呼び出されます。つまり、毎回アプリケーション全体が再描画されるということです。

src\index.js

const root = document.getElementById('root')
observe(knightPosition =>
	ReactDOM.render(<Board knightPosition={knightPosition} />,
	root)
)

そこで加えるのが、新たなルートコンポーネント(Example)となるモジュールsrc/components/example.jsです。アプリケーションモジュールsrc\index.jsは、原則どおりReactDOM.render()メソッドを1度だけ呼び出します。

src\index.js

import { DndProvider } from 'react-dnd'
import Backend from 'react-dnd-html5-backend'
// import { observe } from './Game'
// import Board from './components/Board'
import Example from './components/example'

function App() {
	return (
		<div>
			<DndProvider backend={Backend}>
				<Example />
			</DndProvider>
		</div>
	)
}

const root = document.getElementById('root')
/* observe(knightPosition =>
	ReactDOM.render(<Board knightPosition={knightPosition} />,
	root)
) */
ReactDOM.render(<App />, root)

新たなモジュールsrc\components\example.jsの中身は、つぎのとおりです。フックとしてuseEffect()を用います。引数に渡すコールバックは、副作用関数と呼ばれ、コンポーネントの再描画が必要になると実行されます。今回の場合は、state変数(knightPos)の値が変わったときです(「useEffect / React Hooks – React入門」および「REACT USEEFFECT HOOK」参照)。英語のコメントは、公式作例に添えられていたもので、のちほど解説します。

src\components\example.js

import React, { useState, useEffect } from 'react'
import Board from './Board'
import { observe } from '../Game'

const containerStyle = {
	width: 500,
	height: 500,
	border: '1px solid gray',
}
const ChessboardTutorialApp = () => {
	const [knightPos, setKnightPos] = useState([1, 7])
	// the observe function will return an unsubscribe callback
	useEffect(() => observe(newPos => setKnightPos(newPos)))
	return (
		<div>
			<div style={containerStyle}>
				<Board knightPosition={knightPos} />
			</div>
		</div>
	)
}
export default ChessboardTutorialApp

アプリケーションモジュールsrc/index.jsのコード全体はつぎに示したとおりです(コード001)。

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

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { DndProvider } from 'react-dnd'
import Backend from 'react-dnd-html5-backend'
import Example from './components/example'

function App() {
	return (
		<div>
			<DndProvider backend={Backend}>
				<Example />
			</DndProvider>
		</div>
	)
}
const root = document.getElementById('root')
ReactDOM.render(<App />, root)

子コンポーネントとなったモジュールsrc\components\Board.jsからは、上記のsrc\components\example.jsにルートコンポーネントとしての処理を切り分けて移します。

src\components\Board.js

// import { DndProvider } from 'react-dnd'
// import Backend from 'react-dnd-html5-backend'

function Board({ knightPosition }) {

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

もっともこのまま試すと、つぎのようなエラーが示されるはずです。

Error: Multiple observers not implemented.

モジュールsrc\Game.jsに定めた関数observe()は、コールバック(observer)の重複設定を禁じていました。ところが、副作用関数はレンダリングのたびに実行されてしいます。とりあえずこの判定を外せば、コールバック関数が上書きされて、アプリケーションは動くようになるでしょう(図001)。盤面のモジュールsrc/components/Board.jsの記述全体を、以下のコード002にまとめます。

src\Game.js

export function observe(o) {
	/* if (observer) {
		throw new Error('Multiple observers not implemented.')
	} */
	observer = o
	emitChange()
}

図001■ナイトの駒が正しく動かせる

図001

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

src/components/Board.js

import React from 'react';
import BoardSquare from './BoardSquare'
import Knight from './Knight'

const boardStyle = {
	width: '100%',
	height: '100%',
	display: 'flex',
	flexWrap: 'wrap'
};
const squareStyle = { width: '12.5%', height: '12.5%'};
const Board = ({ knightPosition: [knightX, knightY] }) => {
	const renderSquare = (i) => {
		const x = i % 8;
		const y = Math.floor(i / 8);
		return (
			<div
				key={i}
				style={squareStyle}
			>
				<BoardSquare x={x} y={y}>
					{renderPiece(x, y)}
				</BoardSquare>
			</div>
		);
	}
	const renderPiece = (x, y) => {
		const isKnightHere = knightX === x && knightY === y
		return isKnightHere ? <Knight /> : null
	}
	const squares = Array.from(new Array(64), (element, index) => renderSquare(index));
	return (
		<div style={boardStyle}>
			{squares}
		</div>
	);
}
export default Board;

副作用とは

Reactの公式ドキュメントには、つぎのように説明されています(「副作用フック」)。

これまでに React コンポーネントの内部から、外部データの取得や購読 (subscription)、あるいは手動での DOM 更新を行ったことがおありでしょう。これらの操作は他のコンポーネントに影響することがあり、またレンダーの最中に実行することができないので、われわれはこのような操作を “副作用 (side-effects)“、あるいは省略して “作用 (effects)” と呼んでいます。

02 副作用をクリーンアップする

つぎの副作用関数を呼び出す前に、前の実行に対する後処理を加えたい場合があるでしょう。副作用関数の戻り値に関数を定めれば、その後処理となります。コールバックの重複を許さないのであれば、つぎの副作用を実行する前に、クリアしてしまえばよいのです。前掲のモジュールsrc\components\example.jsが関数useEffect(()を呼び出す際に添えられていたつぎのコメントは、そのことを意味しています。

the observe function will return an unsubscribe callback
observe関数は登録削除のコールバックを返す

モジュールsrc\Game.jsに定めた関数observe()の戻り値として、コールバック(observer)消去の関数を返せば、重複禁止のif文が戻せます。

src\Game.js

export function observe(o) {
	if (observer) {
		throw new Error('Multiple observers not implemented.');
	}

	return () => observer = null;
}

03 副作用の実行が依存するプロパティを定める

コールバック重複のエラーは消えたものの、副作用をコンポーネント再描画のたびに実行する必要はありません。そういうとき、副作用の実行が依存するプロパティを定めるのが、useEffect()関数の第2引数です。プロパティは配列に収めて渡してください。今回コンポーネントを再レンダリングしたいのは、ナイトの位置であるstate変数(knightPos)の値が変わったときです。

そこで、state変数knightPosが変わったら副作用関数を実行したいのであれば、第2引数はつぎのように与えます。

src/components/example.js

const ChessboardTutorialApp = () => {
	const [knightPos, setKnightPos] = useState([1, 7])

	// useEffect(() => observe(newPos => setKnightPos(newPos)))
	useEffect(() => observe(newPos => setKnightPos(newPos)), [knightPos])

}

けれど、副作用関数をよく見てください。ナイトの位置を直接処理しているわけではありません。処理するコールバックを関数observe()に渡しているだけです。このコールバック関数そのものは、ナイトの位置がどう動こうと変わりません。つまり、はじめに1度定めたら済むということです。その場合は、第2引数に空の配列[]を渡してください。後処理の関数はコンポーネントが破棄されるときにも実行されますので、残しておいてよいでしょう。

src/components/example.js

const ChessboardTutorialApp = () => {

	// useEffect(() => observe(newPos => setKnightPos(newPos)))
	useEffect(() => observe(newPos => setKnightPos(newPos)), [])

}

useEffect()関数は、レンダリングが済んでから実行されます。DOMを手で書き替えるような場合に使えるでしょう。また、今回のようにコンポーネントツリーの外にある処理(src\Game.js)をレンダーに反映させるという場合に用います。APIによるデータの読み込みも、よく挙げられる例のひとつです。useEffect()関数の構文を、つぎの表001にまとめました。副作用フックuseEffect()を定めたルートモジュールsrc/components/example.jsの全体は、以下のコード003のとおりです。

表001■useEffect()関数

useEffect()
引数 第1引数は、副作用があるかもしれない命令型の処理を行う関数。レンダリングの結果が画面に反映された後に動作する。第2引数に、副作用が依存する値の配列を与えられる。
  1. 副作用関数: デフォルトではレンダーが済むたびに動作する。
  2. 依存配列: 副作用が依存するプロパティの配列。指定にしたがって、副作用関数が実行される。
    • 指定しないと、再描画のたびに実行される。
    • 配列要素のプロパティが変わったときに実行される。
    • 空の配列[]を渡すと、はじめのマウント時にのみ実行される。
副作用関数

つぎの変化に対して実行される。

  • 親コンポーネントの再描画。
  • propsの値。
  • ステートフックのstate変数の値。

戻り値に後処理の関数を定めることができる。つぎのときに実行される。

  • コンポーネントが破棄されるとき。
  • つぎの副作用関数を実行する前。
戻り値 なし。

コード003■副作用フックを定めたルートモジュール

src/components/example.js

import React, { useState, useEffect } from 'react'
import Board from './Board'
import { observe } from '../Game'

const containerStyle = {
	width: 500,
	height: 500,
	border: '1px solid gray',
}
const ChessboardTutorialApp = () => {
	const [knightPos, setKnightPos] = useState([1, 7])
	useEffect(() => observe(newPos => setKnightPos(newPos)), [])
	return (
		<div>
			<div style={containerStyle}>
				<Board knightPosition={knightPos} />
			</div>
		</div>
	)
}
export default ChessboardTutorialApp

04 複数のコールバックを扱う

公式作例ではもうひとつ、モジュールsrc/Game.jsobserve()関数に渡すコールバックが複数もてるよう配列(observers)に収められています。もっとも、今のところナイトの駒の位置を定める関数ひとつしか使いません。あとあと、加えたい処理が増えた場合に備えてということでしょう。戻り値のクリーンアップ関数も、クロージャに保持したコールバック(o)と照らし合わせたうえで、配列から除いています。

src/Game.js

// let observer = null;
let observers = []
function emitChange() {
	// observer(knightPosition)
	observers.forEach((o) => o && o(knightPosition))
}
export function observe(o) {
	/* if (observer) {
		throw new Error('Multiple observers not implemented.');
	}
	observer = o; */
	observers.push(o)

	// return () => observer = null;
	return () => {
		observers = observers.filter((t) => t !== o)
	}
}

これで、ゲームのロジックを定めるモジュールsrc/Game.jsもでき上がりました。つぎのコード004に全体を掲げます。また、以下のサンプル001はCodeSandboxに公開した今回の作例です。

コード004■ゲームのロジックを定めるモジュール

src/Game.js

import React from 'react';let knightPosition = [1, 7];
let observers = []
function emitChange() {
	observers.forEach((o) => o && o(knightPosition))
}
export function observe(o) {
	observers.push(o)
	emitChange();
	return () => {
		console.log('clear:')
		observers = observers.filter((t) => t !== o)
	}
}
export function moveKnight(toX, toY) {
	knightPosition = [toX, toY];
	emitChange();
}
export function canMoveKnight(toX, toY) {
	const [x, y] = knightPosition
	const dx = toX - x
	const dy = toY - y
	return (
			(Math.abs(dx) === 2 && Math.abs(dy) === 1) ||
			(Math.abs(dx) === 1 && Math.abs(dy) === 2)
	)
}

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

Create React App + React DnD


作成者: 野中文雄
作成日: 2020年04月19日


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