HTML5テクニカルノート
Create React App 入門 02: クリックしたマス目にXをつける
- ID: FN2101002
- Technique: ECMAScript 2015
- Library: React 17.0.1
Create React Appのひな形からチュートリアルと同じマルバツゲームをつくる「Create React App 入門」シリーズ第2回は、クリックしたマス目に「X」印をつけます。9つのマス目のデータやその操作をどこで行い、コンポーネント間でどのようにやり取りするのかがポイントです。
01 JSX要素のリストは配列にする
規則にしたがった複数の要素は、配列をつくってJSXに渡すと、順に取り出して差し込んでくれます(「リストと key」)。盤面のモジュールsrc/components/Board.js
のJSXにおけるつぎのような3マス1行の要素を配列でつくることにしましょう。
src/components/Board.jsconst Board = () => { return ( <div> <div className="board-row"> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> </div> ); };
要素は別に用意したデータにもとづいてつくることが少なくありません。けれど、今回はマス目をつくる関数(renderSquare()
)に引数の数値を渡すだけです。空の要素3つの配列さえあれば足ります。このとき使うのがArray.from()メソッドです。さらに、第2引数としてマップ関数を与えれば、その戻り値を要素に収めた新たな配列が返されます。なお、Array()
関数に要素数だけ定めた空要素の配列には、Array.prototype.map()
メソッドは使えません(「Array.from()メソッド」参照)。また、関数の引数に与えたアンダースコア(_
)ひと文字は、関数の中で使わないという意味を示すことがあります。
const 新たな配列 = Array.from(Array(要素数), (第1引数配列の要素, インデックス) => 新たな配列に収める要素)
盤面のモジュールsrc/components/Board.js
に加える3マス1行の要素を返す関数(renderRow()
)は、つぎのように定めました。配列で動的に要素をつくるときは、それぞれkey
プロパティに一意の値を与えてください。
src/components/Board.jsconst Board = () => { const renderSquare = (i) => // <Square value={i} />; <Square value={i} key={i} />; const renderRow = (start) => <div className="board-row"> {Array.from(Array(3), (_, index) => renderSquare(start + index) )} </div>; return ( <div> {/* <div className="board-row"> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> <div className="board-row"> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className="board-row"> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> */} {renderRow(0)} {renderRow(3)} {renderRow(6)} </div> ); };
02 データをルートコンポーネントにもたせる
マルバツのデータは9マス分まとまっていないと、勝ち負けの判定などの処理ができません。そこで、ルートコンポーネントsrc/components/App.js
にもたせることにしましょう。盤面のコンポーネント(Board
)にまとめることも、できないではありません。けれど、盤面はデータの表示、データとその操作はルートコンポーネントとするのが、わかりやすい切り分けです。
データをもって子コンポーネントに渡し、参照させるためには、src/components/App.js
は関数でなくクラスコンポーネントに書き直さなければなりません。クラスコンポーネントは、つぎのようにReact.Component
を継承します。必ず備えなければならないのが、render()
メソッドです。関数コンポーネントに定めていたreturn
文を、この本体に移します。そして、インスタンスにデータを収める特別なプロパティがstate
です。constructor()
の中で、オブジェクトのかたちでプロパティにデータを与えて初期化してください(「constructor()」参照)。9マスのデータ(squares
)は配列にして、要素は確認用にすべて文字列'X'にしました。空要素の配列に引数の値を埋め込むために用いたのがArray.prototype.fill()
メソッドです。
src/components/App.js親コンポーネントから属性(import React from 'react'; // function App() { class App extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill('X') }; } render() { return ( <div className="game"> {/* <Board /> */} <Board squares={this.state.squares} /> </div> ); }; }
squares
)のかたちで与えられた値は、関数コンポーネントの引数(props
)から参照できます。その値を、さらにバケツリレーで子コンポーネント(Square
)に属性として渡しているのがつぎのsrc/components/Board.js
のコードです。これでルートコンポーネントのデータの値が、9つのマス目にそれぞれ示されます(図001)。
src/components/Board.js// const Board = () => { const Board = (props) => { const renderSquare = (i) => // <Square value={i} key={i} />; <Square value={props.squares[i]} key={i} />; };
図001■9つのマス目にデータの文字が示された
03 クリックしたマス目に印をつける
マス目ははじめ空白にし、クリックしたら印をつけるようにしましょう。このときも、データを書き替えるのはルートコンポーネントです。けれど、クリックするのはマス目ですから、今度は子から親への逆バケツリレーになります。テンプレートの要素がクリックされたことを捉えるのは、onClick
イベントです(「イベント処理」参照)。子コンポーネント(src/components/Square.js
)がprops
から参照して呼び出すハンドラ関数(onClick()
)は、親コンポーネント(src/components/Board.js
)が与えます。
src/components/Square.jssrc/components/Board.jsconst Square = (props) => ( // <button type="button" className="square"> <button type="button" className="square" onClick={props.onClick} > {props.value} </button> );
const Board = (props) => { const renderSquare = (i) => // <Square value={props.squares[i]} key={i} />; <Square value={props.squares[i]} key={i} onClick={() => props.onClick(i)} />; };
逆バケツリレーの最後尾でコールバック(onClick
)を受け取ったルートコンポーネント(src/components/App.js
)は、ハンドラとして自らのメソッド(handleClick()
)を呼び出します(「コンポーネントに(onClick のような)イベントハンドラを渡すには?」参照)。引数(i
)に渡されるのがクリックしたマス目のインデックスです。プロパティstate
は、constructor()
呼び出しのあとは直に書き替えることはできず、メソッドsetState()
を用いなければなりません。引数はオブジェクトで、変更するプロバティ(squares
)に新たな値を与えます。値となる配列は、スプレッド構文...
により複製したうえで要素を書き替えました。また、setState()
メソッドの引数オブジェクトはオブジェクトの分割代入を用いて渡しています。
src/components/App.jsimport React from 'react'; class App extends React.Component { constructor(props) { this.state = { squares: Array(9).fill(null) // 'X') }; this.handleClick = this.handleClick.bind(this); } handleClick(i) { const squares = [...this.state.squares]; squares[i] = 'X'; this.setState({ squares }); }; render() { return ( <div className="game"> {/* <Board squares={this.state.squares} /> */} <Board squares={this.state.squares} onClick={(i) => this.handleClick(i)} /> </div> ); }; }
注意すべき点として、イベントハンドラのメソッド(handleClick()
)がthis
を参照する場合は、あらかじめconstructor()
の本体でFunction.prototype.bind()
によりthis
をバインドしておくようにしましよう(「関数をコンポーネントインスタンスにバインドするには?」参照)。これではじめはnull
で空白にしたマス目に、クリックするとX印がつきます(図002)。
図002■クリックしたマス目にX印がつく
ノート01■データの変更にはイミュータビリティが大切
前掲コードでは、変更するプロバティ(squares
)に、値の配列はスプレッド構文...
で複製してsetState()
メソッドにより設定しました。もとのデータを直に書き替えない、これがイミュータビリティ(immutability)です(「イミュータビリティは何故重要なのか」参照)。
オブジェクトの中身に直接手を加えると、Reactがいつどのような変更があったのか、検出に手間取ります。新たなデータで上書きすれば、変更がはっきりし、その影響範囲とくに描画の更新も最適化されるのです。
また、イミュータビリティは、機能の実装をシンプルにしたり、バグが見つけやすくなるという利点もあります。
書き替えた3つのモジュールは、つぎのコード001にまとめます。また、CodeSandboxに以下のサンプル001を掲げました。
コード001■クリックしたマス目にXをつける
src/components/App.js
import React from 'react';
import Board from './Board';
import './App.css';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null)
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(i) {
const squares = [...this.state.squares];
squares[i] = 'X';
this.setState({ squares });
};
render() {
return (
<div className="game">
<Board
squares={this.state.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
);
};
}
export default App;
import Square from './Square';
const Board = (props) => {
const renderSquare = (i) =>
<Square
value={props.squares[i]}
key={i}
onClick={() => props.onClick(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;
const Square = (props) => (
<button
type="button"
className="square"
onClick={props.onClick}
>
{props.value}
</button>
);
export default Square;
サンプル001■Create React App: Tic Tac Toe 02
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月22日 FN2001001「Create React App 入門 02: クリックしたマス目にXをつける」を大幅に改訂。
Copyright © 2001-2021 Fumio Nonaka. All rights reserved.