HTML5テクニカルノート
Create React App + React DnD 03: React DnD公式サイトの作例に書き替える ー 副作用フックuseEffect()を使って
- ID: FN2004002
- Technique: ECMAScript 2015
- Library: React 16.13.1
前回は、React DnDサイトの「Tutorial」でつくられるドラッグ&ドロップのサンプルコードを、少し手直ししてつくりました。この「Tutorial」には、でき上がりと称するCodeSandboxの作例も公開されています。でも、コードを見ると、解説と微妙に違っているようです。それどころか、文中にひとことも触れられていないモジュール(src/components/example.js
)まで加わっています。そこで今回は、前回のコードにさらに手を加えて、この公式サイトの作例に書き替えてみましょう。ただし、前回に引き続き、細かいところは手直ししました。
01 useEffect()を使う
前回つくった作例「React DnD 02: Chess board and lonely Knight」で気になるのは、おおもとのアプリケーションモジュールsrc\index.js
がReactDOM.render()
メソッドを呼び出すコードです。関数observe()
に渡したコールバック関数は、ナイトの駒の位置(knightPosition
)が変わるたびに呼び出されます。つまり、毎回アプリケーション全体が再描画されるということです。
src\index.jsconst 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.jsimport { 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.jsimport 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.jsexport function observe(o) { /* if (observer) { throw new Error('Multiple observers not implemented.') } */ observer = o emitChange() }
図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.jsexport 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.jsconst ChessboardTutorialApp = () => { const [knightPos, setKnightPos] = useState([1, 7]) // useEffect(() => observe(newPos => setKnightPos(newPos))) useEffect(() => observe(newPos => setKnightPos(newPos)), [knightPos]) }
けれど、副作用関数をよく見てください。ナイトの位置を直接処理しているわけではありません。処理するコールバックを関数observe()
に渡しているだけです。このコールバック関数そのものは、ナイトの位置がどう動こうと変わりません。つまり、はじめに1度定めたら済むということです。その場合は、第2引数に空の配列[]
を渡してください。後処理の関数はコンポーネントが破棄されるときにも実行されますので、残しておいてよいでしょう。
src/components/example.jsconst 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引数に、副作用が依存する値の配列を与えられる。
|
副作用関数 |
つぎの変化に対して実行される。
戻り値に後処理の関数を定めることができる。つぎのときに実行される。
|
戻り値 | なし。 |
コード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.js
のobserve()
関数に渡すコールバックが複数もてるよう配列(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
- Create React App + React DnD 01: ドラッグ&ドロップの前に ー クリックで動かす
- Create React App + React DnD 02: ドラッグ&ドロップで動かす
- Create React App + React DnD 03: React DnD公式サイトの作例に書き替える ー 副作用フックuseEffect()を使って
作成者: 野中文雄
作成日: 2020年04月19日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.