HTML5テクニカルノート
Create React App 入門 10: 条件によってハンドラは無効にする ー useMemoを使って
- ID: FN2102004
- Technique: ECMAScript 2015
- Library: React 17.0.1
Create React Appのひな形からチュートリアルと同じマルバツゲームをつくる「Create React App 入門」シリーズ最終回は、メモ化の応用例のご紹介です。メモ化したコールバック関数を、条件によって無効にします。
01 ゲームが終わったらonClickハンドラは無効にする
src/components/GameContext.js
モジュールでマス目のクリックイベントに定められたコールバック(handleClick()
)は、勝ちが決まったらゲームの処理を先に進めないようにしました。もっとも、ハンドラ関数そのものは、クリックするたびに呼び出されます。
src/components/GameContext.jsexport const GameProvider = ({ children }) => { const handleClick = useCallback((i) => { if (finished) { return; } }, [finished, history, stepNumber, xIsNext]); };
今回のお題は、ゲームが終わったらハンドラのコールバックをなくしてしまうということです。ゲーム終了をマス目のコンポーネントに伝えるため、コンテクストのモジュールsrc/components/GameContext.js
は、終了のフラグ(finished
)をvalue
プロパティに加えます。
src/components/GameContext.jsexport const GameProvider = ({ children }) => { return ( <GameContext.Provider value={{ finished, }} > </GameContext.Provider> ); };
マス目のコンポーネントモジュールsrc/components/Square.js
はonClick
イベントハンドラに、ゲーム終了フラグ(finished
)で判別するつぎのような式を与えればよいでしょう。これで、ゲームが決着したらハンドラのコールバック関数そのものが呼ばれなくなります。
src/components/Square.jsconst 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.jsexport 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.jsconst Square = ({ id }) => { return ( <button onClick={finished ? null : () => onClick(id)} > </button> ); };
上記ハンドラの式をそのままuseCallback
フックに渡すとつぎのようになります。
src/components/Square.jsconst 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
でコールバック関数をメモ化することもできるのです。useCallback
をuseMemo
で書き替えれば、つぎのような構文になります(「useCallback」参照)。
const メモ化された関数 = useMemo(() => コールバック関数, [依存配列]);
もっとも、このような回りくどい書き方をする必要は普通ありません。そのためのuseCallback
フックです。でも、今回のように関数でない値(null
)を含めたいときは、useMemo
フックが使えます。マス目のコンポーネントモジュールsrc/components/Square.js
には、onClick
イベントのコールバック関数をつぎのように定めればよいでしょう。
src/components/Square.jsconst 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 入門
- Create React App 入門 01: 3×3マスのゲーム盤をつくる
- Create React App 入門 02: クリックしたマス目にXをつける
- Create React App 入門 03: マルバツで勝ち負けを決める
- Create React App 入門 04: クラスのコンポーネントをuseState()で関数に書き替える
- Create React App 入門 05: useContextで状態をコンポーネントツリー内に共有する
- Create React App 入門 06: アプリケーションのロジックをコンテクストに切り出す
- Create React App 入門 07: ゲームの履歴をさかのぼる
- Create React App 入門 08: useMemoフックで無駄な再計算を省く
- Create React App 入門 09: useCallbackフックで無駄な処理を省く
- Create React App 入門 10: 条件によってハンドラは無効にする ー useMemoを使って
作成者: 野中文雄
作成日: 2021年02月12日
Copyright © 2001-2021 Fumio Nonaka. All rights reserved.