サイトトップ

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

HTML5テクニカルノート

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


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

01 データをルートコンポーネントにもたせる

マルバツのデータは9マス分まとまっていないと、勝ち負けの判定などの処理ができません。そこで、ルートコンポーネントsrc/components/App.jsにもたせることにしましょう。盤面のコンポーネント(Board)にまとめることもできないではありません。けれど、盤面はデータの表示、データとその操作はルートコンポーネントとするのが、わかりやすい切り分けです。

データをもって子コンポーネントに渡し、参照させるためには、src/components/App.jsは関数でなくクラスコンポーネントに書き直さなければなりません。クラスコンポーネントは、つぎのようにReact.Componentを継承します。必ず備えなければならないのが、render()メソッドです。関数コンポーネントに定めていたreturn文を、この本体に移します。そして、インスタンスにデータを収める特別なプロパティがstateです。constructor()の中で、オブジェクトのかたちでプロパティにデータを与えて初期化してください(「constructor()」参照)。9マスのデータ(squares)は配列にして、要素は確認用にすべて文字列'X'にしました。

src/components/App.js

// 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 = (props) => {
	const renderSquare = (i) =>
		// <Square value={i} />;
		<Square
			value={props.squares[i]}
		/>

};

図001■9つのマス目にデータの文字が示された

図001

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

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

src/components/Square.js

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

src/components/Board.js

const Board = (props) => {
	const renderSquare = (i) =>
		<Square

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

};

逆バケツリレーの最後尾でイベント(onClick)を受け取ったルートコンポーネント(src/components/App.js)は、ハンドラとして自らのメソッド(handleClick())を呼び出します(「コンポーネントに(onClick のような)イベントハンドラを渡すには?」参照)。引数(i)に受け取るのがマス目のインデックスです。プロパティstateは、constructor()呼び出しのあとは直に書き替えることはできず、メソッドsetState()を用いなければなりません。引数はオブジェクトで、変更するプロバティ(squares)に新たな値を与えます。値となる配列は、スプレッド構文...により複製したうえで要素を書き替えました。

src/components/App.js

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: squares});
	};
	render() {
		return (
			<div className="game">
				<Board

					onClick={(i) => this.handleClick(i)}
				/>
			</div>
		);
	}
}

注意すべき点として、イベントハンドラのメソッド(handleClick())がthisを参照する場合は、あらかじめconstructor()の本体でFunction.prototype.bind()によりthisをバインドしておくようにしましよう(「関数をコンポーネントインスタンスにバインドするには?」参照)。これではじめはnullで空白にしたマス目に、クリックするとX印がつきます(図002)。

図002■クリックしたマス目にX印がつく

図002

データの変更にはイミュータビリティが大切

前掲コードでは、変更するプロバティ(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: 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 React from 'react';
import Square from './Square';

const Board = (props) => {
	const renderSquare = (i) =>
		<Square
			value={props.squares[i]}
			onClick={() => props.onClick(i)}
		/>
	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>
		</div>
	);
};

export default Board;

src/components/Square.js

import React from 'react';

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

export default Square;

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

Create React App 入門


作成者: 野中文雄
作成日: 2020年01月03日


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