• Home
  • About
    • taichi photo

      taichi

      ars longa vita brevis.

    • Learn More
    • Twitter
    • LinkedIn
  • Posts
    • All Posts
    • All Tags
  • Projects

Reactチュートリアル解説

20 Nov 2018

Reading time ~9 minutes

Reactチュートリアル解説

Reactとは

React.js(以下、React)は、Facebookが作った動的なUIを作るためのJavaScriptのフレームワークです。

ブラウザ上でのユーザーによる入力などの操作を高速に、しかも簡単にビュー(HTML)に反映ができるものとして現在では非常に人気のあるフレームワークの1つです。

簡単に中身を説明すると、Reactは以下の図のようにユーザーからの操作を仮想DOMに反映させ、そのDOMとビューの差分を自動で更新することで高速でしかも簡単に動的なUIを作ることを実現しています。

react_concept

今回は、Reactの公式チュートリアルに従いながらブラウザ上で動く「○×ゲーム」を作ることによってReactの基礎的な操作方法を学習していきます。

完成品はこちらから事前に確認することができます。あらかじめ確認しておくと、全体像を把握しながら作業を進めることができるので良いでしょう。

ここでは以下の順で機能を実装して行きます。

  • 操作によってビューが変化する
  • ○×ゲームとして遊べる
  • 勝負が決まった時に勝者を示す
  • ゲーム中の動作の履歴を記録する
  • 前にさかのぼってゲームの展開を確認できる

なお、本カルキュラムでは基本的なJavaScript(ES6)の文法を既に理解している前提で進めます。 まだ、JavaScript(ES6)の学習を終えていな人はまずそちらを先に学習することを推奨します。

環境構築

まずは、Reactを使用するための環境構築をしていきます。

Reactが正常に機能するためには、node.jsというJavaScriptがサーバーサイド言語として機能するための環境が必要です

node.jsのインストール

こちらから使用しているOSの種類に合わせてnode.jsをインストールをしてください。

インストールが完了したら、ターミナルまたはコマンドプロンプト(以下、ターミナル)で以下のコマンドを実行してください。

$ node --version

正しくバージョンが表示されていればnode.jsのインストールは完了です。

Reactのインストール

node.jsのインストールが終わったら、さっそく、Reactのインストールをします。

$ npm install --global create-react-app

これで、Reactで開発を行う環境の構築をすることができました。

初期設定

それでは早速ReactでWebアプリの開発を始めましょう。

フォルダを作成

まずは、開発用のフォルダを作成します。 以下のコマンドをターミナルで実行してください。

#任意のディレクトリに移動
$ cd documents

#reactフォルダの作成
$ create-react-app react-tutorial 

#パッケージのインストール(今回はパッケージを編集しないので省略可能)
$ npm install

create-react-app <フォルダ名>コマンドを実行するとReactに必要なパッケージを含んだフォルダを生成してくれます。

今回は公式チュートリアルにある○×ゲームを作成するのでreact-tutorialという名前のフォルダを作りました。

中身の確認

生成したフォルダの中身を確認してみましょう。

react-tutorial
	|- node_modules
		|- reactに付随するmoduleなど
	|- public
		|- htmlファイルなど
	|- src
		|- css,jsファイルなど
	|- .gitignore
	|- package.json
	|- README.md
	|- yarn.lock

重要なところのみ説明していきます。

まず、node_modulesフォルダはnode.jsのパッケージを大量に保管しているところです。開発するものに応じて必要なパッケージをここに記載します。(今回は特に追加せずデフォルトのまま使用します) 先ほど使用したように、npm installをターミナルで実行するとここに書かれているパッケージを全て自動でインストールしてくれます(今回のようにデフォルトのまま使用する場合は省略可能)。

publicフォルダ内にあるindex.htmlは、ウェブサイトの基盤になり最初に表示される画面の部分です。 しかし、Reactの場合はユーザーの操作に合わせて動的にDOMを生成し、それに応じて画面を変えるのでここではJavasSriptを実行する対象の<div id="root"></div>と<head>タグをHTMLファイル内で使って、index.jsファイルのidがrootである箇所を実行させています。

srcフォルダには自分の書いたコードを保管していきます。ここにjsファイルなどを追加し編集して行きます。

ブラウザで開いてみる

では実際に、これらのコードがどのように表示されるか確かめましょう。まずローカルのサーバーを立ち上げます。

$ cd react_tutorial
$ npm start
# ctrl + c でサーバーを停止。

上手く立ち上がれば、http://localhost:3000/でデフォルトのプロジェクトをブラウザー上で確認できます。下のページが表示されたら成功です。

react_top

Tutorial用にファイルを編集

それでは、いよいよ今回作る○×ゲーム用にファイルを編集していきます。 基本となる、cssファイルとjsファイルは最初から用意されているので、まずはそれらを追加していきましょう。

まずは、以下のコマンドを実行して、srcフォルダ内のファイルを削除してください。

$ rm -f src/*

次に、index.cssをsrcフォルダ内に作り、以下のように編集してください。

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

ここではcssファイルの説明はしませんが、興味のある人はそれぞれの値を変更してみるなどして試してみると良いでしょう。

次に、index.jsをsrcフォルダ内に作り、以下のように編集してください。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';


class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

それでは順を追って説明していきます。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

まずここでは、必要なモジュールと使用するcssファイルをインポートしています。

次に、一番下に記述されているここの部分をみてください。

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

ここでは、index.htmlで指定したように、idにrootを指定することによってGameコンポーネントでレンダリングされたもの(DOM)を一番最初に呼び出しています。

コンポーネントというのはクラスで書かれていて、renderメソッド内で記述されたビューを表示させます。

renderメソッド内で先程の<Game />というように書けば指定した部分に、特定のコンポーネントをレンダリングすることもできます。

通常renderメソッドの中身はJSXというもの(これもfacebookが開発した独自のタグ記述)で記述されており、HTMLに近い形でビューやユーザーの操作に伴う動作を記述することができます。 まだ、見慣れない表現などが多いかもしれませんが、扱っているうちに次第に慣れてくるので、全てを理解できなくても焦らず少しずつ進むようにしてください。

次にGameコンポーネントの中身を見ていきましょう。

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

ここでは先ほどと同様の方法で、Boardコンポーネントをレンダリングしています。

{/* status */}とあるのはコメントアウトされた部分で、後々書き換えていく予定ですので、今は無視して構いません。

Gameのrenderメソッド内にも<Board />というのがあるのでBoardコンポーネントをみていきます。

class Board extends React.Component {
    renderSquare(i) {
      return <Square />;
    }
    render() {
      const status = 'Next player: X';
      return (
        <div>
          <div className="status">{status}</div>
          <div className="board-row">
            {this.renderSquare(0)}
            {this.renderSquare(1)}
            {this.renderSquare(2)}
          </div>
          <div className="board-row">
            {this.renderSquare(3)}
            {this.renderSquare(4)}
            {this.renderSquare(5)}
          </div>
          <div className="board-row">
            {this.renderSquare(6)}
            {this.renderSquare(7)}
            {this.renderSquare(8)}
          </div>
        </div>
      );
    }
}

Boardコンポーネントはこの様になっています。ここでは3×3のマスをdivを使って作り、レンダリングしています。

まずrenderメソッド内を見ていくと、

render() {
  const status = 'Next player: X';
  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        {this.renderSquare(0)}
        {this.renderSquare(1)}
        {this.renderSquare(2)}
      </div>
      <div className="board-row">
        {this.renderSquare(3)}
        {this.renderSquare(4)}
        {this.renderSquare(5)}
      </div>
      <div className="board-row">
        {this.renderSquare(6)}
        {this.renderSquare(7)}
        {this.renderSquare(8)}
      </div>
    </div>
  );
}

このようになっています。

JSXで、変数を埋め込む場合は{}で変数を囲むだけで使用することができます。 ここでは、renderメソッド内で定数statusを定義してそれを{status}と書いて呼び出しています。

また、Boardコンポーネント内にはrenderSquareというメソッドが定義されています。

JSXでこういったメソッドを呼び出す際には、

this.(メソッド名)

このようにして呼び出すことができます。 もちろんメソッドに引数も渡すことができます(やりかたは後述)。

次に、renderSquareメソッドを見ていくと、<Square />とあるのでSquareコンポーネントを見ていきます。

class Square extends React.Component {
    render() {
      return (
        <button className="square">
          {/* TODO */}
        </button>
      );
    }
}

Squareコンポーネント内はこのようになっています。 ここではボタンをレンダリングしていますね。

ここまで編集をし終えたら、実際にブラウザで確認してみましょう。 (すでに開いている場合はctl + rでリロードしてください)

$ npm start

これで全体像が見えました。 今回は以下のように全体のコンポーネントが構成されていることがわかったのではないでしょうか。

react_conponent

Reactでは今回の場合、それぞれレンダリングする側される側の関係から、

  • GameコンポーネントはBoardコンポーネントの親コンポーネント
  • BoardコンポーネントはSquareコンポーネントの親コンポーネント

とみなします。

Game,Board,Squareの3つのコンポーネントがそれぞれの階層に応じてしっかりとわけられています。 これがReactのメリットの1つです。 こうすることで機能が増えてきても階層に応じて役割が分かれているのでコードが整理しやすくなります。

データを受け渡す

コンポーネントについて理解をしたところで、コンポーネント間でデータを受け渡す方法を学びます。 試しに、Boardコンポーネント内の値をSquareコンポーネントに渡してみましょう。

それぞれのコンポーネントの内容を以下のように編集してください。 まず、データを渡すBoardコンポーネントを以下のように編集してください。

class Board extends React.Component {
    renderSquare(i) {
      return <Square value={i} />;
    }
  
    render() {
      const status = 'Next player: X';
  
      return (
        <div>
          <div className="status">{status}</div>
          <div className="board-row">
            {this.renderSquare(0)}
            {this.renderSquare(1)}
            {this.renderSquare(2)}
          </div>
          <div className="board-row">
            {this.renderSquare(3)}
            {this.renderSquare(4)}
            {this.renderSquare(5)}
          </div>
          <div className="board-row">
            {this.renderSquare(6)}
            {this.renderSquare(7)}
            {this.renderSquare(8)}
          </div>
        </div>
     );
   }
}

次に、データを受け取る側であるSquareコンポーネントは以下のように編集してください。

class Square extends React.Component {
    render() {
      return (
        <button className="square">
          {this.props.value}
        </button>
      );
    }
}

Reactでは、popsを使うことで、親コンポーネントから子コンポーネントにデータを渡すことができます。

今回は、Boardコンポーネント内でrenderSquareメソッドを使うときに、iとしてそれぞれの引数(0 ~ 8)を使用し、その引数を今度はSquareコンポーネントをレンダリングするときにvalueとしてSquareコンポーネントに引き渡しています。

Squareコンポーネント側では、this.props.valueと記述することで親コンポーネントから渡されたvalueを使用することができます。

これでマスの中に数字が表示されるはずです。 実際にブラウザで確認してみましょう。 (すでに開いている場合はctl + rでブラウザをリロードしてください)

クリックしたら×と表示されるようにする

それではいよいよユーザーの操作に応じて画面が変わるような機能を実装しましょう。 ここでは、試しにSquareコンポーネントのボタンをクリックしたら「✖️」と表示されるようにしましょう。

Squareコンポーネントを以下のように編集してください。

class Square extends React.Component {
    constructor() {
      super();
      this.state = {
        value: null,
      };
    }
    render() {
        return (
          <button
            className="square"
            onClick={() => this.setState({value: 'X'})}
          >
            {this.state.value}
          </button>
        );
    }
}

それでは順を追って説明します。

まず、Reactではthis.stateを使って変数の値を記憶させることができます。 stateを実装する場合constructorを定義してこのように書いていきます。 constructorでは明示的にsuperメソッドを呼び出す必要があります。

constructor() {
      super();
      this.state = {
        value: null,
      };
    }

ここでは、valueの値をnull(何もない状態)で記憶させています。

また、stateで保存された値は{this.state.value}のように記述することによって表示させることができます。

次に、<bottun>タグ内でonClickを使いvalueの値をXに変更しています。

<button
    className="square"
    onClick={() => this.setState({value: 'X'})}
  >

onClickというのは、イベントハンドラといわれるものであり、今回はボタンがクリックされたことを検知すると{}内の処理が行われます({}内はES6のアロー関数を採用しています)。

stateを変更する場合は

this.setState({value: 'X'})

というようにsetStateを呼べば値が変更されます。

なので最終的に、

<button className="square" onClick={() => this.setState({value: 'X'})}>
    {this.props.value}
  </button>

というようにすることによって、最初はnullであったため何も表示されていなかったマスが、クリックされることによりvalueの値がXに変化し、{this.state.value}によって画面も変化するという流れが実装できました。

ReactのイベントハンドラはonClick以外にも代表的なものに以下のようなものがあります。 他にも気になる方は調べてみると良いでしょう。

イベントハンドラ 発生条件
onClick 要素やリンクをクリックした時に発生
onChange フォーム要素の選択、入力内容が変更された時に発生
onLoad ページや画像の読み込みが完了した時に発生
onError 画像の読み込み中にエラーが発生した時に発生

OとXを交代させる

現状だとクリックしてもXとしか表示されないのでボタンをクリックするたびにOとXが入れ替わるようにしたいです。 そのためには、Squareコンポーネントのボタンタグがクリックされるたびに、valueの値をsetStateを使って入れ替えれば良いのですが、今後OとXを交互に入れ替えるだけでなく、勝敗が決まったかどうかを判定したりする機能を作る際に、現状のように各マスがstateを保持している状態では毎回各マスに問い合わせる必要があり、管理がしづらいです。

状態を親コンポーネントに渡す

そこで、ここでは上の階層であるBoardコンポーネントに9つのボタンの状態をもたせて管理するように変更をします。

SquareコンポーネントとBoardコンポーネントを以下のように変更してください。

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

class Board extends React.Component {
    constructor() {
      super();
      this.state = {
        squares: Array(9).fill(null),
      };
    }
    //以下同じ

Squareコンポーネントはstateを持たなくなったため、constructorを削除し、Boardコンポーネントにconstrucorを移しました。

また、この場合Squareコンポーネントはrenderメソッドのみからなるコンポーネントとなります。 Reactではこのようにrenderメソッドのみからなるコンポーネントはfunctional componentsとして書くことができるため、今回はSquareコンポーネントをfunctional componentsに書き換えました。

この場合this.propsと今までしてたのが、propsとなるのに注意してください。

交代させる

それでは、いよいよクリックに応じてvalueに入る値をOとXで交代させる機能を実装します。

Boardコンポーネントを以下のように編集してください。

class Board extends React.Component {
    constructor() {
      super();
      this.state = {
        squares: Array(9).fill(null),
        xIsNext: true, //追加
      };
    }
    handleClick(i) {
      const squares = this.state.squares.slice(); //squares配列をコピー
      squares[i] = this.state.xIsNext ? 'X' : 'O'; //true or falseで分岐
      this.setState({
        squares: squares,
        xIsNext: !this.state.xIsNext, //真偽値を交代させる
      });
    }
    renderSquare(i) {
      return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
    }
    render() {
      const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); //次のプレイヤーが表示されるようにする
      return (
        <div>
          <div className="status">{status}</div>
          <div className="board-row">
            {this.renderSquare(0)}
            {this.renderSquare(1)}
            {this.renderSquare(2)}
          </div>
          <div className="board-row">
            {this.renderSquare(3)}
            {this.renderSquare(4)}
            {this.renderSquare(5)}
          </div>
          <div className="board-row">
            {this.renderSquare(6)}
            {this.renderSquare(7)}
            {this.renderSquare(8)}
          </div>
        </div>
      );
    }
  }

ここでは、各マスがクリックされるたびに、BoardコンポーネントのhandleClickメソッドを発動し、新しくsquares配列を生成した後、valueの値をO → XまたはO → X に切り替え、その値をSquareコンポーネントが受け取り表示させるという機能が実装されています。

それでは順に説明していきます。

まずは、Boardコンポーネントのconstructorをみていきましょう。

constructor() {
      super();
      this.state = {
        squares: Array(9).fill(null),
        xIsNext: true, //追加
      };
    }

ここでは、0から8までの9つのマスの値をまとめて配列として管理し、同時にnull代入しています。 また、xIsNextという変数の値にtrueを代入しています。

このxIsNextはこの後に使用しますが、マスがクリックされるたびに真偽値(true or false)を入れ替え、trueのときにXを、falseのときにOをvalueに代入するような機能を実装するときに使用します。

次に、renderSquareメソッドの中身を確認していきます。

renderSquare(i) {
      return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
    }

ここでは、Squareコンポーネントの呼び出しと同時に変数と、メソッドの受け渡しをしています。 変数(value)の子コンポーネントへの引き渡し方法については既に説明していますが、メソッドも同様の方法で子コンポーネントに引き渡すことができます。

ここでは、handleClickメソッドを設定した、onClickメソッドを子コンポーネント(Square)に引き渡しています。

それでは、handleClickメソッドの中身をみてみましょう。

handleClick(i) {
      const squares = this.state.squares.slice();
      squares[i] = this.state.xIsNext ? 'X' : 'O'; //プレイヤーで分岐
      this.setState({
        squares: squares,
        xIsNext: !this.state.xIsNext, //○と×を交代させる
      });
    }

ここではまず、9つのマスを配列として保存したsquaresをslice()メソッドを使っ新しくコピーしています(なぜわざわざコピーするのかは今後履歴表示機能とタイムトラベル機能を実装するためです)。

次に、条件演算子を使って、xIsNextの値がtrueのときにはXを、xIsNextの値がfalseのときにはOをsquares[i]に代入しています。

この?を使った条件演算子はあまり見慣れない方も多いかと思いますが以下のように定義することができるので是非覚えておきましょう。

<値を代入する変数> = (<条件式>) ? <条件式がtrueのときに代入する値> : <<条件式がfalseのときに代入する値> 

これは、renderメソッド内で次のプレイヤーを表示するためにも使われいるので確認してみてください。

const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

最後に、XかOが代入されたsquaresと、真偽値を反転させる!メソッドによって真偽の入れ替わったxIsNextがsetStateによって保存されます。

これによって、1回目のクリックでは、squareにXが入りその後xIsNextがfalseに入れ替わり、2回目のクリックではsquareにOが入り、その後xIsNextがtrueに入れ替わるといった機能を実装させています。

こうして作られた値をvalue, onClickメソッドとしてSquareコンポーネントに引き渡しています。

こうして親コンポーネントから引き渡された子コンポーネント(Square)は以下のようにthis.props.onClick()、this.props.valueと記述することで渡された変数やメソッドを実行することができます。

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

勝敗を判定させる

さて、ここまでの実装で、ブラウザ上でOXゲームを遊ぶことまではできるようになりました。 しかし、現在のままではどちらが勝ったのか勝敗を自動的に判断することができません。

そこで、ここでは勝負がついたときに勝者を表示する機能を実装していきます。

以下のcalculateWinner関数をファイルの最後に追加してください。

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],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

この関数は、引数で9マスの状態を全て受け取り、どちらかが勝ったとき(縦か横か斜めに3つ同じ記号が並んだとき)に勝った方の記号をreturnで返すものです。

それでは、この関数をBoardコンポーネントのrenderメソッド内で呼び出しましょう。

render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // 以下同じ

これで、どちらかが勝ったときにWinner: XまたはWinner: Oと表示されるようになりました。

同じ場所をクリックできないようにする

加えて、勝敗がついたときにそれ以降マスを押しても何も起こらないようにするのと、これまですでに埋まっているマスをクリックしても上書きできてしまっていたのですでに埋まっているマスをクリックしても何も起こらないようにします。

handleClickメソッドの中身を以下のように編集してください。

  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

ここでは、条件分岐(if文)を使って勝者が既にいる状態または指定したマスに何かしら値が入っている場合、以降のコードを実行する前にreturnを実行し、何も起きないようにしました。 (||はorと同じ意味です。)

履歴を記録する機能をつけよう

ゲームは完成しましたがさらに機能を拡張していきます。 各動作がどのような状態だったかわかるようにいつでも過去の状態に戻れるようにしていきます。

少し大変そうな感じがしますが、既にslice()メソッドを使って毎ターンごとに新しい配列として盤面を格納していたものを利用すれば良いだけです。

今回はhistoryという配列を作成して、その1つの要素に今までのsquares(9マスの盤面)を格納するようにします。そうすることで各動作ごとの状態を保持することができます。

historyは以下のような構造になるのを想像してください。

history = [
  // 1番最初
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // 1手目を動かした後
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // 2手目を動かした後
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

状態を親コンポーネントに渡す

それでは早速、historyを作成していくのですが、重要なのはどのコンポーネントにhistoryの状態を持たせるかです。 今回の場合、Boardコンポーネントでは3×3のボードを作るのに専念させたほうがいいのでよりトップレベルのGameコンポーネントにその役割を任せます。 そうした場合に、序盤でSquareコンポーネントからBoardコンポーネントに状態を引き上げたように、今回もBoardコンポーネントの状態を以下の手順にしたがってGameコンポーネントに引き上げていきます。

  • Gameコンポーネントに状態(constructor)を移行
  • Boardコンポーネント内のthis.state.squares[i]をthis.props.squares[i]に変更
  • Boardコンポーネント内のthis.handleClick(i)をthis.props.handleClick(i)に変更

まずは、Boardコンポーネントを以下のように変更してください。

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }
  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }], //squaresを要素に持つ配列にする
      xIsNext: true
    };
  }
  handleClick(i) {
    var history = this.state.history;
    var current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

BoardコンポーネントにあったconstructorとhandleClickメソッドを全てGameコンポーネントに移動させました。

親コンポーネントから値やメソッドを受け渡されるため、各表記がthis.props.squares[i]、this.props.handleClick(i)に変わったことに注意してください。

Gameコンポーネントのconstructorを見てください、

constructor() {
      super();
      this.state = {
        history: [{
          squares: Array(9).fill(null)
        }], //squaresを要素に持つ配列にする
        xIsNext: true
      };
    }

ここで、squaresを要素に持つhistoryという配列を定義しています。

また、handleClick内も確認して見ましょう。

handleClick(i) {
      var history = this.state.history;
      var current = history[history.length - 1];
      const squares = current.squares.slice();
      if (calculateWinner(squares) || squares[i]) {
        return;
      }
      squares[i] = this.state.xIsNext ? 'X' : 'O';
      this.setState({
        history: history.concat([{
          squares: squares
        }]),
        xIsNext: !this.state.xIsNext,
      });
    }

ここでは、history配列に対してconcat()メソッドを使うことで、最新の盤面をhistory配列に追加しています。

履歴を表示させる

それでは、ここから履歴を表示するようにします

Gameコンポーネントのrenderメソッド内を以下のように編集してください。

render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }

ここでは、map()メソッドを使ってhistory配列の中身を複数レンダリングしています。 JavaScriptでは、map()メソッドは以下のように使用することができます。

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

今回の場合だと、各ターンごとにOnClickを実装したボタンを含む<li>タグを生成しています。 (JumpToメソッドはまだ定義していません。)

それでは実際に、各ターンごとに履歴が表示されるかブラウザで確認していきましょう。

react_error

Reactを起動すると一見正しく表示されているように見えますが、ブラウザ上でコンソール画面を確認するとこのようにエラーが表示されていることに気づきます。 (ブラウザ上で右クリック → 「検証」 → 「console」タブで確認できます)

これにはある理由があります。

これは少し難しい話になりますが、 Reactではレンダリングを最小限にするために変更されたものしか再レンダリングをしないような仕組みになっているため、配列の中でレンダリングする要素の順番が変わったり、今回のように要素(盤面)を新しく追加するような場合、変更する要素のみを識別させる必要があります。

そのために、通常の場合だと配列の各要素にユニークな(一意性のある)keyプロパティを指定し、識別して行きます。

今回の場合だと、配列の順番(move)がユニークな値になるため、配列の各要素のkeyにmoveを指定することによって、再レンダリングする必要がある要素のみを認識してレンダリングしてくれるようになります。

movesの中身を以下のように変更してください。

const moves = history.map((step, move) => {
  const desc = move ?
    'Move #' + move :
    'Game start';
  return (
    <li key={move}>
      <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
    </li>
  );
});

これで、先ほどのエラーは解消されたかと思います。

タイムトラベルを実装する

現状だとボタンを押してもjumpToメソッドを定義していないので実装していきます。

Gameコンポーネントを以下のように編集してください。

class Game extends React.Component {
    constructor() {
      super();
      this.state = {
        history: [{
          squares: Array(9).fill(null)
        }], 
        xIsNext: true,
        stepNumber: 0 //追加
      };
    }
    handleClick(i) {
      const history = this.state.history.slice(0, this.state.stepNumber + 1); //変更
      const current = history[history.length - 1]; //変更
      const squares = current.squares.slice();
      if (calculateWinner(squares) || squares[i]) {
        return;
      }
      squares[i] = this.state.xIsNext ? 'X' : 'O';
      this.setState({
        history: history.concat([{
          squares: squares
        }]),
        xIsNext: !this.state.xIsNext,
        stepNumber: history.length //追加
      });
    }

    //追加
    jumpTo(step) {
      this.setState({
        stepNumber: step,
        xIsNext: (step % 2) ? false : true,
      });
    }

    render() {
      const history = this.state.history;
      const current = history[this.state.stepNumber];//変更
      const winner = calculateWinner(current.squares);

      let status;
      if (winner) {
        status = 'Winner: ' + winner;
      } else {
        status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
      }

      //追加
      const moves = history.map((step, move) => {
        const desc = move ?
          'Move #' + move :
          'Game start';
        return (
          <li>
            <button onClick={() => this.jumpTo(move)}>{desc}</button>
          </li>
        );
      });
      
      return (
        <div className="game">
          <div className="game-board">
            <Board
              squares={current.squares}
              onClick={(i) => this.handleClick(i)}
            />
          </div>
          <div className="game-info">
            <div>{status}</div>
            <ol>{moves}</ol>
          </div>
        </div>
      );
    }
  }    

ここでは、

  • constructorにstepNumber: 0を追加
  • jumpToメソッドを定義
  • handleClockメソッドを編集
  • renderメソッド内を編集

といった編集を行いました。

それでは順に見ていきましょう。

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

stepNumberは、何ターン目かを記録するものとして使います。

jumpTo(step) {
      this.setState({
        stepNumber: step,
        xIsNext: (step % 2) ? false : true,
      });
    }

jumpToメソッドでは、クリックが行われるごとにstepNumberをアップデートし、またstepが偶数の場合にxIsNextがtrueになるようにsetStateをしています。

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

ここでは、”go back”ボタンを押し、特定の地点(stepNumber)からゲームをやり直した場合、その地点以降の盤面をすべてやり直せるように編集しています。

具体的には、this.state.historyとしていたところをthis.state.history.slice(0, this.state.stepNumber + 1)と記述し直すことでそれを可能にしています。

render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    // the rest has not changed

最後に、renderメソッド内ですが、historyのうち常に指定したStepNumberをレンダリングできるようcurrentの値をhistory[this.state.stepNumber];と記述し直しました。

ここまで編集をし終えたら、最後にブラウザできちんと動作するか確認しましょう。

正しく動いていれば完成です。

終わりに

これでReactのチュートリアルは一通り終わりました。 お疲れ様でした。

内容を最初から全て理解するのは難しいですが、Reactの全体的な仕組みや構成がなんとなくわかったのではないでしょうか。

最初に述べたようにReactは動的なUIを作る際に非常に便利なフレームワークです。 今回作成した公式チュートリアル以外にも様々なサンプルがありますので、色々自分で調べてやってみるとよいかと思います。

チュートリアルの最後には以下のような追加課題がいくつか出されています。 自分自身の力試しも込めて挑戦してみましょう。

問題

  • 動作の場所を「6」ではなく「(1, 3)」というフォーマットで表示させてみましょう。
  • 動作リストで現在選択されているアイテムを太字にしてみましょう。
  • マスをハードコーディングする代わりに2つのループを使って書き直してみましょう。
  • 動作リストを昇順、降順で並び替えるトグルボタンを追加してみましょう。
  • どちらかが勝った時に、どの手で勝ったか3つのマスをハイライトさせてみましょう。


TechnologyReact Share Tweet +1