サイトトップ

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

HTML5テクニカルノート

Create React App 入門 05: useContextで状態をコンポーネントツリー内に共有する


コンテクストは、親コンポーネントのデータを子やさらにその下のコンポーネントから参照できるようにする仕組みです。useContextフックが、コンポーネントツリー内の関数コンポーネントからコンテクストを使えるようにします。前回は、クラスコンポーネントをuseStateフックで関数に書き替えました。今回はさらに、useContextフックを加えてみることがお題です。

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

まず、親のコンポーネントでコンテクストをつくらなければなりません。その関数がReact.createContext()です。引数は、コンテクストとして子に参照を許すデータの初期値です。複数ある場合には、オブジェクトで渡してください。

コンテクストをつくると、Context.Providerコンポーネントが使えるようになります。このコンポーネントでJSXテンプレートを包めば、ラップされた子孫からコンテクストが参照できるようになる仕組みです。このとき、valueプロパティで子どもたちに渡すデータを定めます。つまり、React.createContext()メソッドに渡した初期値はあくまでデフォルトで、実際に参照されるのはvalueプロパティの値です。初期値が用いられるのは、Context.Providerコンポーネントにラップされていないコンポーネントが、コンテクストにアクセスしようとしたときくらいでしょう。

なお、React.createContext()メソッドが返したコンテクストは、他のモジュール(子コンポーネント)から参照する場合はexportしておかなければなりません。


import React, { createContext } from 'react';

export const コンテクスト = createContext(初期値);
function App() {

	return (
		<コンテクスト.Provider value={{プロパティ}}>

		</コンテクスト.Provider>
	);
}

親コンポーネントのモジュールsrc/components/App.jsは、つぎのようにコンテクスト(GameContext)をつくってプロバイダ(GameContext.Provider)でラップします。初期値はわかりやすいように変数(initialContext)に定めました。コンテクストに加えたデータ(squaresonClick)はプロパティを介さずに参照できますので、JSXの子コンポーネント(Board)から除いてしまって構いません。

src/components/App.js

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

const initialContext = { squares: Array(9).fill(null) };
export const GameContext = createContext(initialContext);
function App() {

	return (
		<GameContext.Provider value={{squares, onClick: handleClick}}>
			<div className="game">
				{/* <Board
					squares={squares}
					onClick={(i) => handleClick(i)}
				/> */}
				<Board />

			</div>
		</GameContext.Provider>
	);
}

データは、子コンポーネントがコンテクストから参照できるようになりました。モジュールsrc/components/Board.jsuseContextフックを用いるよう書き替えたのが、つぎのコードです。コンテクスト(GameContext)からプロパティ(squares)を取り出しますので、関数コンポーネントの引数(props)は除いてしまいます。また、クリックイベントのハンドラ(onClick)は、もはやバケツリレーはしません。ただし、孫コンポーネント(Square)はハンドラに渡す引数(i)が要りますので、新たにプロパティ(id)として加えました。

src/components/Board.js

import { useContext } from 'react';
import { GameContext } from './App';

// const Board = (props) => {
const Board = () => {
	const { squares } = useContext(GameContext);
	const renderSquare = (i) =>
		<Square
			// value={props.squares[i]}
			value={squares[i]}
			id={i}

			// onClick={() => props.onClick(i)}
		/>;

};

孫コンポーネントのモジュールsrc/components/Square.jsは、クリックイベントのハンドラ(onClick)はコンテクストから取り出し、プロパティで受け取った引数(props.id)を渡して呼び出します。

src/components/Square.js

import { useContext } from 'react';
import { GameContext } from './App';

// const Square = (props) => (
const Square = (props) => {
	const { onClick } = useContext(GameContext);
	return (
		<button

			// onClick={props.onClick}
			onClick={() => onClick(props.id)}
		>

		</button>

// );
	);
};

02 子から孫への中継をさらに減らす

子のコンポーネントのモジュールsrc/components/Board.jsから孫コンポーネントには、前掲コードで新たに加えたプロパティのほか、もうひとつ(value)渡されていました。けれど、この参照は孫コンポーネントがコンテクストから取り出せます。すると、子コンポーネントはコンテクストを見る必要がありません。

src/components/Board.js

// import { useContext } from 'react';
// import { GameContext } from './App';

const Board = () => {
	// const { squares } = useContext(GameContext);
	const renderSquare = (i) =>
		<Square
			// value={squares[i]}

		/>;

};

孫のモジュールsrc/components/Square.jsはコンテクストから必要なデータ(squares)を参照し、関数コンポーネントの引数オブジェクトから分割代入で受け取ったプロパティ(id)により取り出します。

src/components/Square.js

// const Square = (props) => {
const Square = ({ id }) => {
	// const { onClick } = useContext(GameContext);
	const { onClick, squares } = useContext(GameContext);
	return (
		<button

			// onClick={() => onClick(props.id)}
			onClick={() => onClick(id)}
		>
			{/* {props.value} */}
			{squares[id]}
		</button>
	);
};

子の盤面のコンポーネント(Board)はプロパティ(id)をひとつ孫に渡しているだけで、コンテクストには触れなくなりました。プロパティも受け取らず、表示に専念することになりました。孫のコンポーネント(Square)が必要なデータを、直にコンテクストから取り出して使えるようになったからです。ふたつのモジュールの記述全体を、つぎのコード001にまとめます。

コード001■コンテクストを使った盤面とマス目のコンポーネントモジュール

src/components/Board.js

import Square from './Square';

const Board = () => {
	const renderSquare = (i) =>
		<Square
			id={i}
			key={i}
		/>;
	const renderRow = (start) =>
		<div className="board-row">
			{Array.from(Array(3), (_, index) => (
				renderSquare(start + index)
			))}
		</div>;
	return (
		<div>
			{renderRow(0)}
			{renderRow(3)}
			{renderRow(6)}
		</div>
	);
};

export default Board;

src/components/Square.js

import { useContext } from 'react';
import { GameContext } from './App';

const Square = ({ id }) => {
	const { onClick, squares } = useContext(GameContext);
	return (
		<button
			type="button"
			className="square"
			onClick={() => onClick(id)}
		>
			{squares[id]}
		</button>
	);
};

export default Square;

03 ゲーム情報の表示をコンポーネントに切り出す

バケツリレーを気にすることもなくなりましたので、切り出せるコンポーネントはどんどん分けましょう。ルートコンポーネントのモジュールsrc/components/App.jsで、つぎの差し手あるいは勝者の情報(status)を表示するJSX要素(<div>)がそれです。

src/components/App.js

function App() {

	const winner = calculateWinner(squares);
	const status = (winner) ?
		`Winner: ${winner}` :
		`Next player: ${xIsNext ? 'X' : 'O'}`;
	return (
		<GameContext.Provider value={{squares, onClick: handleClick}}>
			<div className="game">

				<div className="game-info">
					<div>{status}</div>
				</div>
			</div>
		</GameContext.Provider>
	);
}

準備として、ルートコンポーネントのモジュールsrc/components/App.jsの状態に勝者(winner)の変数を加えます。つぎの差し手(xIsNext)とともにあとでコンテクストに含めますので、初期値(initialContext)も与えておきましょう。

src/components/App.js

// const initialContext = { squares: Array(9).fill(null) };
const initialContext = {
	squares: Array(9).fill(null),
	xIsNext: true,
	winner: null,
};

function App() {

	const [winner, setWinner] = useState(initialContext.winner);
	const handleClick = (i) => {

		// const winner = calculateWinner(_squares);
		const _winner = calculateWinner(_squares);
		setWinner(_winner);
		// if (winner) {
		if (_winner) {
			setFinished(true);
		}
	};
	/* const winner = calculateWinner(squares);
	const status = (winner) ?
		`Winner: ${winner}` :
		`Next player: ${xIsNext ? 'X' : 'O'}`; */

}

ゲーム情報を表示するために切り出した子モジュールsrc/components/GameInfo.jsは、コンテクストから必要なデータ(winnerxIsNext)が受け取れます。それらの値にもとづいて、コンポーネントに表示すべき情報(status)を決めればよいのです(コード002)。

コード002■ゲーム情報を表示するコンポーネントモジュール

src/components/GameInfo.js

import { useContext } from 'react';
import { GameContext } from './App';

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;

改めて、ゲーム情報表示の子コンポーネント(GameInfo)を差し込むルートモジュールsrc/components/App.jsのコードです。Providerコンポーネントのvalueに必要な変数(xIsNextwinner)を加えます。

src/components/App.js

import GameInfo from './GameInfo';

function App() {

	return (
		// <GameContext.Provider value={{squares, onClick: handleClick}}>
		<GameContext.Provider value={{xIsNext, squares, winner, onClick: handleClick}}>
			<div className="game">

				{/* <div className="game-info">
					<div>{status}</div>
				</div> */}
				<GameInfo />
			</div>
		</GameContext.Provider>
	);
}

これで、ルートコンポーネントのデータが、コンテクストによりコンポーネントツリーの中で共有することができました。モジュールsrc/components/App.jsの全体の記述は、つぎのコード003のとおりです。各モジュールの記述とアプリケーションの動きは、CodeSandboxに公開した以下のサンプル001でお確かめください。

コード003■ルートコンポーネントのモジュール

src/components/App.js

import { createContext, useState } from 'react';
import Board from './Board';
import GameInfo from './GameInfo';
import './App.css';

const initialContext = {
	squares: Array(9).fill(null),
	xIsNext: true,
	winner: null,
};
export const GameContext = createContext(initialContext);
function App() {
	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={{xIsNext, squares, winner, onClick: handleClick}}>
			<div className="game">
				<Board />
				<GameInfo />
			</div>
		</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;
}

export default App;

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

Create React App 入門


作成者: 野中文雄
作成日: 2021年01月26日 FN2005005「React Hooks: useContext()フックを使う」を「Create React App 入門」シリーズに組み込むかたちで大幅に改訂。


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