サイトトップ

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

HTML5テクニカルノート

Create React App 入門 10: 条件によってハンドラは無効にする ー useMemoを使って


Create React Appのひな形からチュートリアルと同じマルバツゲームをつくる「Create React App 入門」シリーズ最終回は、メモ化の応用例のご紹介です。メモ化したコールバック関数を、条件によって無効にします。

01 ゲームが終わったらonClickハンドラは無効にする

src/components/GameContext.jsモジュールでマス目のクリックイベントに定められたコールバック(handleClick())は、勝ちが決まったらゲームの処理を先に進めないようにしました。もっとも、ハンドラ関数そのものは、クリックするたびに呼び出されます。

src/components/GameContext.js

export const GameProvider = ({ children }) => {

	const handleClick = useCallback((i) => {

		if (finished) { return; }

	}, [finished, history, stepNumber, xIsNext]);

};

今回のお題は、ゲームが終わったらハンドラのコールバックをなくしてしまうということです。ゲーム終了をマス目のコンポーネントに伝えるため、コンテクストのモジュールsrc/components/GameContext.jsは、終了のフラグ(finished)をvalueプロパティに加えます。

src/components/GameContext.js

export const GameProvider = ({ children }) => {

	return (
		<GameContext.Provider
			value={{

				finished,

			}}
		>

		</GameContext.Provider>
	);
};

マス目のコンポーネントモジュールsrc/components/Square.jsonClickイベントハンドラに、ゲーム終了フラグ(finished)で判別するつぎのような式を与えればよいでしょう。これで、ゲームが決着したらハンドラのコールバック関数そのものが呼ばれなくなります。

src/components/Square.js

const Square = ({ id }) => {
	// const { onClick, history, stepNumber } = useContext(GameContext);
	const { onClick, finished, history, stepNumber } = useContext(GameContext);

	return (
		<button

			// onClick={() => onClick(id)}
			onClick={finished ? null : () => onClick(id)}
		>

		</button>
	);
};

対応するコンテクストモジュールsrc/components/GameContext.jsの修正はつぎのとおりです。ゲームが決着したらコールバックは呼ばれないのですから、終了フラグ(finished)の判定は要らなくなります。他方で、9マスすべてが埋まったら、これもゲーム終了としなければなりません。

src/components/GameContext.js

export const GameProvider = ({ children }) => {

	const handleClick = (i) => {

		// if (finished) { return; }

		// if (_winner) {
		if (_winner || _history.length >= 9) {
			setFinished(true);
		}
	// }, [finished, history, stepNumber, xIsNext]);
	}, [history, stepNumber, xIsNext]);
	const jumpTo = useCallback((step) => {

		// if (winner) {
		if (winner || step >= 9) {
			setFinished(true);
		} else {

		}
	}, [history]);

};

コンテクストのモジュールsrc/components/GameContext.jsは、これででき上がりです。つぎのコード001に記述全体をまとめましょう。

コード001■コールバックの処理を書き直したコンテクストのモジュール

src/components/GameContext.js

import { createContext, useCallback, useState } from 'react';

const initialContext = {
	history: [
		{squares: Array(9).fill(null)},
	],
	xIsNext: true,
	winner: null,
	stepNumber: 0,
};
export const GameContext = createContext(initialContext);
export const GameProvider = ({ children }) => {
	const [history, setHistory] = useState(initialContext.history);
	const [xIsNext, setXIsNext] = useState(initialContext.xIsNext);
	const [finished, setFinished] = useState(false);
	const [winner, setWinner] = useState(initialContext.winner);
	const [stepNumber, setStepNumber] = useState(initialContext.stepNumber);
	const handleClick = useCallback((i) => {
		const _history = history.slice(0, stepNumber + 1);
		const _squares = [..._history[_history.length - 1].squares];
		if (_squares[i]) { return; }
		_squares[i] = xIsNext ? 'X' : 'O';
		setHistory([..._history, { squares: _squares }]);
		setXIsNext(!xIsNext);
		setStepNumber(_history.length);
		const _winner = calculateWinner(_squares);
		setWinner(_winner);
		if (_winner || _history.length >= 9) {
			setFinished(true);
		}
	}, [history, stepNumber, xIsNext]);
	const jumpTo = useCallback((step) => {
		const winner = calculateWinner([...history[step].squares]);
		setWinner(winner);
		setStepNumber(step);
		setXIsNext((step % 2) === 0);
		if (winner || step >= 9) {
			setFinished(true);
		} else {
			setFinished(false);
		}
	}, [history]);
	return (
		<GameContext.Provider
			value={{
				onClick: handleClick,
				finished,
				jumpTo,
				history,
				stepNumber,
				winner,
				xIsNext,
			}}
		>
			{children}
		</GameContext.Provider>
	);
};

function calculateWinner(squares) {
	const lines = [
		[0, 1, 2],
		[3, 4, 5],
		[6, 7, 8],
		[0, 3, 6],
		[1, 4, 7],
		[2, 5, 8],
		[0, 4, 8],
		[2, 4, 6],
	];
	const length = lines.length;
	for (let i = 0; i < length; i++) {
		const [a, b, c] = lines[i];
		const player = squares[a];
		if (player && player === squares[b] && player === squares[c]) {
			return player;
		}
	}
	return null;
}

02 マス目のonClickハンドラをメモ化する

マス目のコンポーネントモジュールsrc/components/Square.jsは、onClickハンドラに条件演算子の式を与えました。けれど、コールバックはやはりメモ化しておきたいところです。

src/components/Square.js

const Square = ({ id }) => {

	return (
		<button

			onClick={finished ? null : () => onClick(id)}
		>

		</button>
	);
};

上記ハンドラの式をそのままuseCallbackフックに渡すとつぎのようになります。

src/components/Square.js

const Square = ({ id }) => {

	const handleClick = useCallback(finished ? null : () => onClick(id)
	, [finished, id, onClick]);
	return (
		<button

			// onClick={finished ? null : () => onClick(id)}
			onClick={handleClick}
		>

		</button>
	);
};

一応動きはするものの、適切ではありません。つぎのような警告が示されます。返される値のひとつnullは関数ではなく、useCallbackが依存を正しく確かめられないからです。フックの第1引数には関数を渡さなければなりません。

React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.

03 useMemoフックでコールバック関数をメモ化する

値をメモ化するのはuseMemoフックでした。そして、値には関数も含まれます。つまり、useMemoでコールバック関数をメモ化することもできるのです。useCallbackuseMemoで書き替えれば、つぎのような構文になります(「useCallback」参照)。


const メモ化された関数 = useMemo(() => コールバック関数, [依存配列]);

もっとも、このような回りくどい書き方をする必要は普通ありません。そのためのuseCallbackフックです。でも、今回のように関数でない値(null)を含めたいときは、useMemoフックが使えます。マス目のコンポーネントモジュールsrc/components/Square.jsには、onClickイベントのコールバック関数をつぎのように定めればよいでしょう。

src/components/Square.js

const Square = ({ id }) => {

	/* const handleClick = useCallback(finished ? null : () => onClick(id)
	, [finished, id, onClick]); */
	const handleClick = useMemo(() => finished ? null : () => onClick(id)
	, [finished, id, onClick]);

};

これで、onClickイベントのコールバック関数はメモ化され、ゲームの決着がついたらハンドラは無効(null)になって呼び出されません。書き直したマス目のコンポーネントモジュールsrc/components/Square.jsの記述全体は、つぎのコード002にまとめたとおりです。

コード002■onClickイベントのコールバック関数をメモ化したマス目のコンポーネントモジュール

src/components/Square.js

import { useContext, useMemo } from 'react';
import { GameContext } from './GameContext';

const Square = ({ id }) => {
	const { onClick, finished, history, stepNumber } = useContext(GameContext);
	const squares = useMemo(() => [...history[stepNumber].squares], [history, stepNumber]);
	const handleClick = useMemo(() => finished ? null : () => onClick(id)
	, [finished, id, onClick]);
	return (
		<button
			type="button"
			className="square"
			onClick={handleClick}
		>
			{squares[id]}
		</button>
	);
};

export default Square;

サンプル001■Create React App: Tic Tac Toe 10

Create React App 入門


作成者: 野中文雄
作成日: 2021年02月12日


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