HTML5テクニカルノート
Create React App 入門 09: useCallbackフックで無駄な処理を省く
- ID: FN2102003
- Technique: ECMAScript 2015
- Library: React 17.0.1
前回は、useMemoフックを用いて、算出値の計算が無駄に繰り返されないようにメモ化しました。今回は関数の呼び出しです。関数コンポーネントに定められた関数も、再描画のたびにつくり直されます。関数の生成をメモ化するのがuseCallback
フックです。
01 useCallbackフックを使う
useCallback
フックの構文は、useMemo
とよく似ています。違いは第1引数の関数が呼び出すコールバックだということです。第2引数には、useMemo
と同じ依存配列を渡します。
import { useCallback } from 'react'; const メモ化された関数 = useCallback(コールバック関数, [依存配列]);
02 コンテクストモジュールのハンドラ関数をメモ化する
まず、コンテクストのモジュールsrc/components/GameContext.js
にuseCallback
フックを用いましょう。ひとつめは、盤面のマス目をクリックしたときに呼び出すハンドラ関数(handleClick()
)です。ハンドラをuseCallback
フックの第1引数、コールバックとして包みます。useMemo
と同じく、第2引数の依存配列を正しく与えることが大切です。
src/components/GameContext.js// import { createContext, useState } from 'react'; import { createContext, useCallback, useState } from 'react'; export const GameProvider = ({ children }) => { // const handleClick = (i) => { const handleClick = useCallback((i) => { // }; }, [finished, history, stepNumber, xIsNext]); };
コンテクストモジュールsrc/components/GameContext.js
に定めるコールバック関数はもうひとつあります。ゲーム情報のコンポーネントのボタンリストをクリックしたときのハンドラ(jumpTo()
)です。つぎのように、useCallback
フックを加えましょう。呼び出す関数をフックでメモ化したモジュールの記述全体は、以下のコード001のとおりです。
src/components/GameContext.jsexport const GameProvider = ({ children }) => { // const jumpTo = (step) => { const jumpTo = useCallback((step) => { // }; }, [history]); };
コード001■呼び出す関数をuseCallbackでメモ化したコンテクストモジュール
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; }
if (finished) { return; }
_squares[i] = xIsNext ? 'X' : 'O';
setHistory([..._history, { squares: _squares }]);
setXIsNext(!xIsNext);
setStepNumber(_history.length);
const _winner = calculateWinner(_squares);
setWinner(_winner);
if (_winner) {
setFinished(true);
}
}, [finished, history, stepNumber, xIsNext]);
const jumpTo = useCallback((step) => {
const winner = calculateWinner([...history[step].squares]);
setWinner(winner);
setStepNumber(step);
setXIsNext((step % 2) === 0);
if (winner) {
setFinished(true);
} else {
setFinished(false);
}
}, [history]);
return (
<GameContext.Provider
value={{
onClick: handleClick,
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;
}
03 盤面のコンポーネントの関数をメモ化する
つぎに、盤面のコンポーネントモジュールsrc/components/Board.js
に、useCallback
フックを用います。マス目(Square
)のJSX要素をつくって返す関数(renderSquare()
)と、それを3つ並べて1行のJSX要素にする関数(renderRow()
)です。とくに、前者は盤面のモジュールが、はじめに一度つくれば済みます(印を書き替えるのはマス目のコンポーネントの役目です)。したがって、依存配列は空[]
にしました。書き直したモジュールの記述全体は、以下のコード002のとおりです。以下のサンプル001をCodeSandboxに公開しましたので、動きや各モジュールの具体的なコードについてはこちらでお確かめください。
src/components/Board.jsimport { useCallback } from 'react'; const Board = () => { // const renderSquare = (i) => const renderSquare = useCallback((i) => ( <Square // />; /> ), []); // const renderRow = (start) => const renderRow = useCallback((start) => ( <div className="board-row"> {/* </div>; */} </div> ), [renderSquare]); };
コード002■useCallbackでコールバック関数をメモ化した盤面のコンポーネントモジュール
src/components/Board.js
import { useCallback } from 'react';
import Square from './Square';
const Board = () => {
const renderSquare = useCallback((i) => (
<Square
id={i}
key={i}
/>
), []);
const renderRow = useCallback((start) => (
<div className="board-row">
{Array.from(Array(3), (_, index) => (
renderSquare(start + index)
))}
</div>
), [renderSquare]);
return (
<div>
{renderRow(0)}
{renderRow(3)}
{renderRow(6)}
</div>
);
};
export default Board;
サンプル001■Create React App: Tic Tac Toe 09
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を使って
作成者: 野中文雄
作成日: 2020年02月08日
Copyright © 2001-2021 Fumio Nonaka. All rights reserved.