サイトトップ

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

HTML5テクニカルノート

Create React App 入門 02: クリックしたマス目にXをつける


Create React Appのひな形からチュートリアルと同じマルバツゲームをつくる「Create React App 入門」シリーズ第2回は、クリックしたマス目に「X」印をつけます。9つのマス目のデータやその操作をどこで行い、コンポーネント間でどのようにやり取りするのかがポイントです。

01 JSX要素のリストは配列にする

規則にしたがった複数の要素は、配列をつくってJSXに渡すと、順に取り出して差し込んでくれます(「リストと key」)。盤面のモジュールsrc/components/Board.jsのJSXにおけるつぎのような3マス1行の要素を配列でつくることにしましょう。

src/components/Board.js

const 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.js

const 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つのマス目にデータの文字が示された

図001

03 クリックしたマス目に印をつける

マス目ははじめ空白にし、クリックしたら印をつけるようにしましょう。このときも、データを書き替えるのはルートコンポーネントです。けれど、クリックするのはマス目ですから、今度は子から親への逆バケツリレーになります。テンプレートの要素がクリックされたことを捉えるのは、onClickイベントです(「イベント処理」参照)。子コンポーネント(src/components/Square.js)がpropsから参照して呼び出すハンドラ関数(onClick())は、親コンポーネント(src/components/Board.js)が与えます。

src/components/Square.js

const Square = (props) => (
	// <button type="button" className="square">
	<button
		type="button"
		className="square"
		onClick={props.onClick}
	>
		{props.value}
	</button>
);

src/components/Board.js

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

import 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印がつく

図002

ノート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;

src/components/Board.js

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;

src/components/Square.js

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


作成者: 野中文雄
作成日: 2021年01月22日 FN2001001「Create React App 入門 02: クリックしたマス目にXをつける」を大幅に改訂。


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