HTML5テクニカルノート
Create React App 入門 05: useContextで状態をコンポーネントツリー内に共有する
- ID: FN2101005
- Technique: ECMAScript 2015
- Library: React 17.0.1
コンテクストは、親コンポーネントのデータを子やさらにその下のコンポーネントから参照できるようにする仕組みです。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
)に定めました。コンテクストに加えたデータ(squares
とonClick
)はプロパティを介さずに参照できますので、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.js
でuseContext
フックを用いるよう書き替えたのが、つぎのコードです。コンテクスト(GameContext
)からプロパティ(squares
)を取り出しますので、関数コンポーネントの引数(props
)は除いてしまいます。また、クリックイベントのハンドラ(onClick
)は、もはやバケツリレーはしません。ただし、孫コンポーネント(Square
)はハンドラに渡す引数(i
)が要りますので、新たにプロパティ(id
)として加えました。
src/components/Board.jsimport { 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.jsimport { 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;
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.jsfunction 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
は、コンテクストから必要なデータ(winner
とxIsNext
)が受け取れます。それらの値
にもとづいて、コンポーネントに表示すべき情報(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
に必要な変数(xIsNext
とwinner
)を加えます。
src/components/App.jsimport 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 入門
- 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月26日 FN2005005「React Hooks: useContext()フックを使う」を「Create React App 入門」シリーズに組み込むかたちで大幅に改訂。
Copyright © 2001-2021 Fumio Nonaka. All rights reserved.