サイトトップ

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

HTML5テクニカルノート

React Hooks: useContext()フックを使う


コンテクストは、親コンポーネントのデータを子やさらにその下のコンポーネントから参照できるようにする仕組みです。useContext()フックが、関数コンポーネントからコンテクストを使えるようにします。「React Hooks: クラスのコンポーネントをuseState()で関数に書き替える」では、React公式サイトのチュートリアルにもとづいた作例(○×ゲーム)のクラスコンポーネントを関数に書き替えました(サンプル001)。このサンプルに、さらにuseContext()フックを加えてみようというのが本稿のお題です。フックの基本的な使い方に慣れていない方は、リンクした記事を先に読まれるとよいでしょう。

サンプル001■React Hooks: Tic Tac Toe 02

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

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

コンテクストをつくると、Context.Providerコンポーネントが使えるようになります。このコンポーネントでテンプレートをラップすれば、ラップされた子孫からコンテクストが参照できるようになる仕組みです。このとき、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)でラップします。初期値は状態変数(history)と関連するため、それがわかりやすいように変数(initialContext)に定めました。コンテクストに加えたデータ(squares)は、テンプレートでは子コンポーネント(Board)のプロパティから除いてしまって構いません。

src/components/App.js

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

const initialContext = { squares: new Array(9) };
export const GameContext = createContext(initialContext);
function App() {
	// const [history, setHistory] = useState([{ squares: new Array(9) }]);
	const [history, setHistory] = useState([{squares: initialContext.squares}]);

	return (
		<GameContext.Provider value={{squares}}>
			<div className="game">
				<Board
					// squares={squares}

				/>

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

これで、子コンポーネントからコンテクストが参照できるようになります。モジュールsrc/components/Board.jsuseContext()フックを用いるよう書き替えたのが、つぎのコードです。コンテクスト(GameContext)から参照するプロパティ(squares)を取り出せば、関数コンポーネントの引数(props)を介さずに済むようになります。

src/components/Board.js

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

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

				// value={props.squares[i]}
				value={squares[i]}

			/>
		);

};

02 コンテクストにデータを加える

コンテクストに新たなデータを加えましょう。親モジュールsrc/components/App.jsの状態変数(finished)です。加え方は、前項の場合と変わりません。コンテクストを参照するのは、やはり子コンポーネント(Board)です。したがって、テンプレートからプロパティは省きます。

src/components/App.js

const initialContext = {

	finished: false
};

function App() {
	// const [finished, setFinished] = useState(false);
	const [finished, setFinished] = useState(initialContext.finished);

	return (
		// <GameContext.Provider value={{squares}}>
		<GameContext.Provider value={{squares, finished}}>
			<div className="game">
				<Board
					// finished={finished}

				/>

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

子のモジュールsrc/components/Board.jsからのプロパティ(finished)の参照の仕方も、前項と同じです。関数の引数オブジェクト(props)からデータを取り出さずに済むようになりました。

src/components/Board.js

const Board = props => {
	// const { squares } = useContext(GameContext);
	const { squares, finished } = useContext(GameContext);
	const renderSquare = i =>
		// props.finished ? (
		finished ? (

		) : (

		);

	);
};

もっとも、子のコンポーネントであれば、データを取り出す参照先が関数コンポーネントの引数(props)かコンテクストかの違いにすぎません。

03 孫のコンポーネントから関数を参照する

親から孫までバケツリレーするデータについては、コンテクストで中抜きができてすっきりします。お題の作例では、マス目のコンポーネント(Square)をクリックしたときに呼び出すイベントハンドラ(onClick)がそれです。コンテクストに加える初期値としては、関数であることが示せれば構いません。実際のデータはプロバイダ(GameContext.Provider)のvalueに与えます。モジュールsrc/components/App.jsにはもう手を加えませんので、以下のコード001にまとめました。

src/components/App.js

const initialContext = {

	onClick: (i) => {}
};

function App() {

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

					// onClick={i => handleClick(i)}
				/>

			</div>

		</GameContext.Provider>
	);
}

コード001■src/components/App.js


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

const initialContext = {
	squares: new Array(9),
	finished: false,
	onClick: (i) => {}
};
export const GameContext = createContext(initialContext);
function App() {
	const [history, setHistory] = useState([{squares: initialContext.squares}]);
	const [stepNumber, setStepNumber] = useState(0);
	const [xIsNext, setXIsNext] = useState(true);
	const [finished, setFinished] = useState(initialContext.finished);
	const handleClick = i => {
		if (finished) {
			return;
		}
		if (stepNumber >= 9) {
			setFinished(true);
			return;
		}
		const _history = history.slice(0, stepNumber + 1);
		const squares = [..._history[_history.length - 1].squares];
		console.log('history:', _history.length, stepNumber);
		if (squares[i]) {
			return;
		}
		const winner = calculateWinner(squares);
		if (winner) {
			setFinished(true);
			return;
		}
		squares[i] = xIsNext ? 'X' : 'O';
		setHistory([..._history, { squares }]);
		setStepNumber(_history.length);
		setXIsNext(!xIsNext);
	};
	const jumpTo = step => {
		setStepNumber(step);
		setXIsNext(step % 2 === 0);
		setFinished(false);
	};
	const _history = [...history];
	const squares = [..._history[stepNumber].squares];
	const winner = calculateWinner(squares);
	const status = winner
		? 'Winner: ' + winner
		: 'Next player: ' + (xIsNext ? 'X' : 'O');
	const moves = _history.map((step, move) => {
		const desc = move ? 'Go to move #' + move : 'Go to game start';
		return (
			<li key={move}>
				<button onClick={() => jumpTo(move)}>{desc}</button>
			</li>
		);
	});
	return (
		<GameContext.Provider value={{squares, finished, onClick: handleClick}}>
			<div className="game">
				<Board />
				<div className="game-info">
					<div>{status}</div>
					<ol>{moves}</ol>
				</div>
			</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;

子のモジュールsrc/components/Board.jsはイベントハンドラ(onClick)を、もはや孫に中継する必要がありません。すると、孫コンポーネント(Square)にハンドラを定めるかどうかも、孫自身に決めさせれば済みます。つまり、その判定のためのデータ(finished)も子には要らなくなるということです。子の関数コンポーネント(Board)は、親からデータを引数(props)に受け取る必要がなくなりました。コンテクストからひとつデータ(squares)を参照しているだけです。

src/components/Board.js

// const Board = props => {
const Board = () => {
	// const { squares, finished } = useContext(GameContext);
	const { squares } = useContext(GameContext);
	const renderSquare = i =>
		/* finished ? (
			<Square key={i} value={squares[i]} />
		) : ( */
			<Square

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

};

孫モジュールsrc/components/Square.jsは、バケツリレーなく、コンテクストから直接必要なデータが受け取れるようになりました。onClickハンドラを備えるかどうかも、コンテクストからデータ(finished)を直接参照すれば判別できるのです。

src/components/Square.js

const Square = props => {
	const { finished, onClick } = useContext(GameContext);
	return (
		// <button className="square" onClick={props.onClick}>
		finished ? (
			<button className="square">
				{props.value}
			</button>
		) : (
			<button className="square" onClick={() => onClick(props.id)}>
				{props.value}
			</button>
		)
	);
};

04 データを必要なコンポーネントが参照する

子のモジュールsrc/components/Board.jsがコンテクストからたったひとつ参照したデータ(squares)も、結局孫コンポーネント(Square)に渡すものです。ただし、子コンポーネントがもつデータ(i)と絡みます。けれど、その値は別のプロパティ(id)として与えていました。つまり、子からプロパティ(value)として受け取らなくても、孫がコンテクストから値を取り出せるということです。子のコンポーネントは、もはや親からデータを受け取らずに済むということにご注目ください。

src/components/Board.js

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

const Board = () => {
	// const { squares } = useContext(GameContext);
	const renderSquare = i =>
		/* finished ? (
			// <Square key={i} value={props.squares[i]} />
			<Square key={i} value={squares[i]} />
		) : */ (
		<Square

			// value={props.squares[i]}
			id={i}
		/>
	);

孫のモジュールsrc/components/Square.jsは、子を中継せず、コンテクストから直に必要なデータが取り出せるようになりました。今回の例のように親・子・孫が連携し、データを使うのはおもに孫という場合は、コンテクストによりコードがすっきりします。

src/components/Square.js

// const Square = props => {
const Square = ({ id }) => {
	// const { finished, onClick } = useContext(GameContext);
	const { squares, finished, onClick } = useContext(GameContext);
	return (
		finished ? (
			<button className="square">
				{/* {props.value} */}
				{squares[id]}
			</button>
		) : (
			// <button className="square" onClick={() => onClick(props.id)}>
			<button className="square" onClick={() => onClick(id)}>
				{/* {props.value} */}
				{squares[id]}
			</button>
		)
	);
};

お題のサンプルのコンテクストを使った書き替えはここまでです。ふたつのモジュールの記述を以下のコード002および003として掲げます。また、動きを確かめられるように、サンプル002をCodeSandboxに公開しました。

コード002■src/components/Board.js


import React from 'react';
import Square from './Square';

const Board = () => {
	const renderSquare = i => (
		<Square
			key={i}
			id={i}
		/>
	);
	const renderRow = (start, end) => {
		const rowSquares = [];
		for (let i = start; i <= end; i++) {
			rowSquares.push(renderSquare(i));
		}
		return rowSquares;
	};
	return (
		<div>
			<div className="board-row">{renderRow(0, 2)}</div>
			<div className="board-row">{renderRow(3, 5)}</div>
			<div className="board-row">{renderRow(6, 8)}</div>
		</div>
	);
};

export default Board;

コード003■src/components/Square.js


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

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

export default Square;

サンプル002■React Hooks: Using Context and useContext() hook


作成者: 野中文雄
更新日: 2020年06月08日 src/components/Square.jsのコードを簡略化。
作成日: 2020年05月25日


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