HTML5テクニカルノート
Create React App 入門 06: アプリケーションのロジックをコンテクストに切り出す
- ID: FN2101006
- Technique: ECMAScript 2015
- Library: React 17.0.1
前回は、ルートコンポーネントの状態を、コンテクスでコンポーネントツリー内に共有しました。今回は、状態とその操作のロジックを、コンテクストのモジュールに切り出してみます。インタフェースと表示に専念するコンポーネントとロジックのモジュールを分ければ、見通しがよくなり、動作の確認や機能の拡張もしやすくなるのです。
01 コンテクストをつくってuseContext()フックで参照する
新たにつくるコンテクストのモジュールsrc/components/GameContext.js
は、つぎのような枠組みにします。鍵になるのは、コンテクストのProvider
を返して、export
することです。ルートコンポーネントは、import
したProvider
でコンポーネントツリーを包みます。ひとつ課題は、子どもたちにどうやってvalue
を渡すかです。これは、つぎの項でご説明します。
src/components/GameContext.jsimport { createContext } from 'react'; const initialContext = { }; export const GameContext = createContext(initialContext); export const GameProvider = (props) => { /* ルートコンポーネントAppから切り出したロジック */ return ( <GameContext.Provider value={{ onClick: handleClick, squares, winner, xIsNext }}> /* コンポーネントツリーにどうやってvalueを渡すか */ </GameContext.Provider> ); };
ロジックを切り出したルートコンポーネントのモジュールsrc/components/App.js
の記述が、つぎのコード001です。Provider
(GameProvider
)でコンポーネントツリーをラップして返すだけの単純なコードになってしまいました。状態にも一切触れません。
コード001■ロジックが切り出されたルートコンポーネントモジュール
src/components/App.js
import { GameProvider } from './GameContext';
import Board from './Board';
import GameInfo from './GameInfo';
import './App.css';
function App() {
return (
<GameProvider>
<div className="game">
<Board />
<GameInfo />
</div>
</GameProvider>
);
}
export default App;
02 ラップしたコンポーネントツリーをprops.childrenで受け取る
前掲コード001で、ルートコンポーネントからProvider
コンポーネントには、プロパティは何も渡されていないように見えるかもしれません。けれど、ラップしたコンポーネントツリーは、props.children
というプロパティとして受け取れるのです。コンテクストのモジュールsrc/components/GameContext.js
で、プロパティをつぎのようにProvider
コンポーネントの子として差し込みます(プロパティは引数からオブジェクトの分割代入で取り出しました)。そうすれば、コンポーネントツリーがコンテクストのvalue
を参照できるようになるのです。
src/components/GameContext.jsexport const GameProvider = ({ children }) => { return ( <GameContext.Provider value={{ onClick: handleClick, squares, winner, xIsNext }}> {children} </GameContext.Provider> ); };
ロジックを切り出したコンテクストのモジュールsrc/components/GameContext.js
の記述全体は、つぎのコード002のとおりです。ロジックはルートコンポーネントからほぼそのまま移しました。状態の保持と操作が、このモジュールに集約されたことになります。
コード002■ロジックを切り出したコンテクストのモジュール
src/components/GameContext.js
import { createContext, useState } from 'react';
const initialContext = {
squares: Array(9).fill(null),
xIsNext: true,
winner: null,
};
export const GameContext = createContext(initialContext);
export const GameProvider = ({ children }) => {
const [squares, setSquares] = useState(initialContext.squares);
const [xIsNext, setXIsNext] = useState(initialContext.xIsNext);
const [finished, setFinished] = useState(false);
const [winner, setWinner] = useState(initialContext.winner);
const handleClick = (i) => {
const _squares = [...squares];
if (_squares[i]) { return; }
if (finished) { return; }
_squares[i] = xIsNext ? 'X' : 'O';
setSquares(_squares);
setXIsNext(!xIsNext);
const _winner = calculateWinner(_squares);
setWinner(_winner);
if (_winner) {
setFinished(true);
}
};
return (
<GameContext.Provider value={{ onClick: handleClick, squares, 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 子孫コンポーネントはコンテキストの読み込みもとを変えるだけ
あと、書き替えなければならないのはコンポーネントツリーの子どもたちです。といっても、それぞれ1行だけ、コンテクスト(GameContext
)のimport
もとを直せば済みます。
src/components/Square.js
src/components/GameInfo.js// import { GameContext } from './App'; import { GameContext } from './GameContext';
一応、これらのコンポーネントモジュールについても、つぎのコード003に全体をまとめておきます。動きが確かめられるよう、以下のサンプル001をCodeSandboxに掲げました。
コード003■コンテキストのimportもとを修正した子・孫のコンポーネントモジュール
src/components/Square.js
import { useContext } from 'react';
import { GameContext } from './GameContext';
const Square = ({ id }) => {
const { onClick, squares } = useContext(GameContext);
return (
<button
type="button"
className="square"
onClick={() => onClick(id)}
>
{squares[id]}
</button>
);
};
export default Square;
import { useContext } from 'react';
import { GameContext } from './GameContext';
const GameInfo = () => {
const { winner, xIsNext } = useContext(GameContext);
const status = (winner) ?
`Winner: ${winner}` :
`Next player: ${xIsNext ? 'X' : 'O'}`;
return (
<div className="game-info">
<div>{status}</div>
</div>
);
};
export default GameInfo;
サンプル001■Create React App: Tic Tac Toe 06
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年01月29日
Copyright © 2001-2021 Fumio Nonaka. All rights reserved.