サイトトップ

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

HTML5テクニカルノート

Create React App 入門 06: アプリケーションのロジックをコンテクストに切り出す


前回は、ルートコンポーネントの状態を、コンテクスでコンポーネントツリー内に共有しました。今回は、状態とその操作のロジックを、コンテクストのモジュールに切り出してみます。インタフェースと表示に専念するコンポーネントとロジックのモジュールを分ければ、見通しがよくなり、動作の確認や機能の拡張もしやすくなるのです。

01 コンテクストをつくってuseContext()フックで参照する

新たにつくるコンテクストのモジュールsrc/components/GameContext.jsは、つぎのような枠組みにします。鍵になるのは、コンテクストのProviderを返して、exportすることです。ルートコンポーネントは、importしたProviderでコンポーネントツリーを包みます。ひとつ課題は、子どもたちにどうやってvalueを渡すかです。これは、つぎの項でご説明します。

src/components/GameContext.js

import { 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.js

export 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;

src/components/GameInfo.js

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 入門


作成者: 野中文雄
作成日: 2021年01月29日


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