6
2

More than 3 years have passed since last update.

Reactチュートリアルをこなしてみてその1

Last updated at Posted at 2020-07-21

Reactチュートリアルをこなしてみてその1

公式チュートリアルをやってみたのでアウトプットします。

リポジトリはこちら

Reactのコンポーネントについて


class ShoppingList extends React.Component {
    render() {
        return {
            <div className="shopping-list">
                <h1>Shopping List for {this.props.name}</h1>
                <ul>
                    <li>Instagram</li>
                    <li>WhatsApp</li>
                    <li>Oculus</li>
                </ul>
            </div>
        }
    }
}

ReactはJSXという書式を使って書いていきます。
上記のコードを見ればわかるようにJavascriptでもHTMLのように書けるような書式といった感じです。
上記のようなコードひとまとまりを指して、Reactではコンポーネントと呼ぶそうです。
ShopppingListはいわゆるクラスに相当する部分ですね。
つまり、コンポーネントはReact.Componentを継承したクラスを指すという見方もできます。
このコンポーネントにいろいろ書いてそれをrender()メソッドでうまいことやったあと、returnで描画したいHTMLを返すというのがReactの基本みたいです。
ちなみにreturn以下のHTML部分はJavascriptで書くと

return React.createElement('div', {className: 'shopping-list'},
  React.createElement('h1', /* ... h1 children ... */),
  React.createElement('ul', /* ... ul children ... */)
);

上記のようになるそうです。
JSXのがわかりやすいですね。
単なるHTMLとはclassclassNameになっているところや<ShoppingList />と書けばそれ全体を指定することもできます。
例えば<div />としてやれば、divタグすべてが指定されます。

Propsについて

では公式チュートリアルのように三目並べを作っていきます。
この三目並べは

  • Square
  • Board
  • Game

の3つのコンポーネントから構成されます。
実はこの3つのコンポーネントは親子関係ができていたりするのですが、それは少しずつ見ていくとしてまずは盤面を作ってみます。
そのためのコードが以下の部分です。

src/index.js

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

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

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がマス単位、Boardが盤面全体に対してのコンポーネントだと思ってください。
ここで重要なのはrenderSquareメソッドでSquareに対して値(value={i})をパスしているところです。
少し複雑なのですが要は

renderSquare(i) { return <Square value={i} />;}

この部分が

{this.renderSquare(i)}

として描画されることになるのですが、そのためにまずrenderSquareメソッドでSquareコンポーネントに対して各マスの値はvalue={i}だよという指示を出します。
すると、Squareコンポーネント側は{this.props.value}でそれを受け取ります。
Reactではこのようにあるコンポーネントからコンポーネントへと情報を流していくように書いていくのですが、そこでパスされていくのがpropsということになるわけですね。
あとはSquareコンポーネントで各マスはボタンだよという定義がなされているので{this.renderSquare(i)}の各部分は0~9のマスのボタンとして描画されるということです。
また、流れでさらっと書いてますがBoardは盤面全体を表すということで上記のような過程を踏んでいます。
つまり、Squareコンポーネントでマスがどういうものなのか(今回はボタン)を定義して、それをBoardで具体的な盤面として表しているという感じです。
よって、BoardはSquareに対して親の関係にあたるということになります。

stateについて

では、次にSquare コンポーネントがクリックされた場合に “X” と表示されるように変えてみます。


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

SquareコンポーネントのbuttonタグにonClickプロパティを追加し、その引数として関数を設定しています。
この場合、マスをクリックしたらアラートがポップアップするということになりますね。
これでアクションを設定したので、あとはSquareコンポーネントがこのアクションを記憶できるように処理を書いていきます。
Reactではそのような処理を定義するのにstateというものを使います。
では書いてみましょう。


class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

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

}

まずコンストラクタを追加します。
コンストラクタは初期化やプリセットの値を用意するために定義するものですが、今回はstateの状態を初期化するために用意することになります。
次にrender()メソッド側も手を入れます。
まず、onClickプロパティの引数をthis.setState()に変更します。
setStateはstateを変更するためのメソッドです。
つまり、現状ではBoardから受け取ったpropsをコンストラクタで引き取りthis.stateにvalueを初期化して代入したあとに、Squareのrender()メソッドにおいてそれを書き換えているという処理になっています。
これで、どのマス目をクリックしてもXマークが表示されることになります。

リファクタリングしていく

基本の処理ができたところでここからはリファクタリングを行って三目並べを完成させていきます。
Reactではこのあと行うState のリフトアップのように実装→リファクタリングを繰り返していくような流れでコードを書いていくのが基本的な流れになるみたいです。

Stateのリフトアップ

さて、先述の通り現在はSquareコンポーネントでstateの状態が管理されています。
しかし、ゲームが今どういう状態なのかということはより上位のコンポーネントで管理をしたい方が都合がいいでしょう。
今回の場合は、BoardでStateを管理し、各Squareにpropsを渡すことでどのマスに何を描画するかを決定するという状態にしたいわけです。
ということでSquareからBoardへとStateの管理を移譲していきます。
では見ていきましょう。


class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state= {
      squares: Array(9).fill(null),
    };
  }

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

まず、constructorをBoardコンポーネントに移します。
ここからBoardでvalueとして0~8の値を渡したあと、SquareのStateでXマークに値を書き換えていたのをBoardから直接個別の値を渡すようにしたいのでrenderSquare()を上記のようにします。
次に、ここからonClickの挙動を変更します。
今、マス目に何が入っているかの管理をBoardコンポーネントに移しましたが、各マスにそれを反映させるためにはSquareがBoardに移したstateを更新できるようにしなければいけません。
しかし、stateはいわゆるprivateなものなので、コンポーネント外からの直接の変更は受け付けません。
なので、BoardからSquareのonClickにstateを変更するような関数を渡して、クリックのアクションでそれを呼び出す形でstateを変更する処理を実装します。
そのためにrenderSquare()を以下のように変更します。


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

そして、Squareコンポーネントを以下のように書き換えます。


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

ここまで来たらひとまず現状を整理しましょう。
まず、BoardからSquareに渡されているpropsにはvalueとonClickなります。
valueはマスの値、onClickはこのあと定義するstateの状態を変更するhandleClick(i)メソッドとなります。
そして、Squareにおいてそれらをthis.props.value及びthis.props.onClick()として呼び出しています。
厳密には以下(チュートリアルから引用)のような処理になるようです。

  1. 組み込みのDOMコンポーネントである <button> に onClick プロパティが設定されているため React がクリックに対するイベントリスナを設定します。
  2. ボタンがクリックされると、React は Square の render() メソッド内に定義されている onClick のイベントハンドラをコールします。
  3. このイベントハンドラが this.props.onClick() をコールします。Square の onClick プロパティは Board から渡されているものです。
  4. Board は Square に onClick={() => this.handleClick(i)} を渡していたので、Square はクリックされたときに this.handleClick(i) を呼び出します。
  5. まだ handleClick() は定義していないので、コードがクラッシュします。Square をクリックすると、“this.handleClick is not a function” といった赤いエラー画面が表示されるはずです。

ちなみにReactではイベント(ここではクリックという行為)に対してon[Event](ex.onClick)、イベントを処理するメソッドには handle[Event](ex.handleClick) という名前を付けるのが慣習となっているようです。
では、未定義のhandleClickを定義していきましょう。


class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});

  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(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>
    );
  }
}


handleClick()メソッド内にset.State()メソッドを入れることでstateの変更を行っています。
では、改めて現状を整理すると

  • SquareコンポーネントはBoard コンポーネントから値を受け取って、クリックされた時はそのことをBoardコンポーネントに伝えるだけになった。
  • Boardのstateが変更されると、個々のSquareコンポーネントもそれに合わせて再度レンダーされる。
  • よって全てのマス目の状態はBoardコンポーネント内で保持され、SquareコンポーネントはBoardコンポーネントに制御された状態となった。

ということになります。

さて、ここで注目するべきはconst squares = this.state.squares.slice();という部分です。
slice()は配列のコピーを作成するメソッドですが、わざわざsquaresのコピーを作成するより、直接squaresを書き換えるような形でいいのでは? という疑問が残ると思います。
それに対する解答が次に見ていくイミュータビリティという考え方です。

イミュータビリティについて

イミュータビリティ(immutability; 不変性)ということで、データを変更する際に元データに対して、書き換えではなく置き換えを行うというアプローチをする考え方です。
チュートリアルでの例を見てみます。


// ミューテート(書き換え)でのアプローチ
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}

// 置き換えでのアプローチ

var player = {score: 1, name: 'jeff'};

var newPlayer = Object.assign({}, player, {score: 2});

//  Now player is unchanged, but newPlayer isn`t {score: 1, name: 'Jeff'}, however {score: 2, name: 'Jeff'}

Object.assignオブジェクトのコピーを作るメソッドです。
第一引数に空のオブジェクト、第二引数にコピーするオブジェクトを指定します。
さらにここで第三引数で参照したいオブジェクト指定することで、コピーしたオブジェクトの参照元を変更できます。
つまり、var newPlayer = Object.assign({}, player, {score: 2});playerオブジェクトをコピーしてscoreプロパティは{score: 2}を参照してくださいという意味になるわけです。
このあたりはオブジェクト指向について少し触れたことがある人は理解しやすいと思います。

では、なぜこういった手法を取るのかというと公式のチュートリアルでは理由が3つ挙げられています。

  1. 複雑な機能が簡単に実装できる

これはこのあと実装するタイムトラベル、つまりは手順の巻き戻しの機能を実装として例が挙げられています。
手順を巻き戻すということは例えば、playerオブジェクトのscoreが変動するということになるのですが、ここで書き換えの手法を使ってしまうとscoreプロパティを変更するのにいくつかの行程を踏む可能性が出てきてしまいます。
上記の例だけでも、手順を戻したり進めたりするだけで其の度にplayer.scoreに値を代入し直さないといけなくなります。
対して、置き換えの手法取ると参照するオブジェクトをplayerとnewPlayerとで切り替えればいいだけなので簡単ですね。

  1. 変更の検出が容易

概ね1と同じような理由ですがチュートリアルからの引用を載せておきます。

ミュータブル (mutable) なオブジェクトは中身が直接書き換えられるため、変更があったかどうかの検出が困難です。ミュータブルなオブジェクト変更の検出のためには、以前のコピーと比較してオブジェクトツリーの全体を走査する必要があります。
イミュータブルなオブジェクトでの変更の検出はとても簡単です。参照しているイミュータブルなオブジェクトが前と別のものであれば、変更があったということです。

  1. React の再レンダータイミングの決定がしやすい

これも上記2つと同じような理由ですね。
Reactはフロントエンドの技術なので画面をどう描画するかということを処理する役割を持ちます。
なので、画面が切り替わったりあるいは画面遷移がなくてもその場で今回のように手順の変更等があり、画面に変更がある場合などはその都度処理をするのですがその際、stateのどの部分に変更があったのかということがわからないと、それを探すのに差分をすべて探してしまいます。そうなると、当然処理は重くなり再描画が遅くなってしまいます。
しかし、置き換えの手法を用いると参照オブジェクトの切り替えだけで事が済むのでReactはstate内のどの部分に変更があったのか検知をして、差分の比較を変更があったオブジェクトのみで行うことができます。
すると、Reactは画面のどの部分を再描画したらいいのか判断をし、変更の必要のない部分は再描画を行わないという処理を実行するので処理に負担をかけずに済むわけですね。

以上、3点については以下の記事が簡潔にわかりやすくまとまっています。

Reactにおけるstateのイミュータビリティ

関数コンポーネント

さて、Boardコンポーネントに盤面の管理を完全に移行したのでSquareコンポーネントはもうpropsを受け取ってそれに基づいて描画内容を返す役割しかありません。
なので、クラスのコンポーネントではなくより簡潔な関数のコンポーネントに書き直します。
関数コンポーネントはstateを持たず、render()メソッドのみを持つコンポーネントのことを指します。


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

このようになります。
クラスではなくなったのでthis.propspropsになりました。
ちなみにそれと合わせてonClick={() => this.props.onClick()}onClick={props.onClick}と書き換えることができるみたいなのでそれに合わせておきます。

手番の処理

先手と後手の概念を付与します。
まずは、Boardコンポーネントのコンストラクタの部分を変更します。


class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }
}

xIsNextプロパティを新たに追加しました。
これを元に真偽値を判断してtrueである場合Xをそうでない場合はOを描画するように処理を書いていきます。
stateの状態を変更するメソッドはhandleClick()メソッドでしたね。
ついでにrender()メソッド内も変更して誰の手番か表示するようにします。


handleClick(i) {
  const squares = this.state.squares.slice();
  // 真偽値判断
  squares[i] = this.state.xIsNext ? 'X':'O';
  // 呼び出されるたびにxIsNextの状態を反転させる
  this.setState({
    squares: squares,
    xIsNext: !this.state.xIsNext,
  });
}

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

勝者の判定

最後に勝者の判定を実装します。
チュートリアルでは以下の関数をコピペしろという支持があるのでコピペします。


function calculateWinner(squares) {

  // squareコンポーネントに与える配列のリスト
  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],
  ];

  // 9 つの square の配列が与えられると、この関数は勝者がいるか適切に確認し、'X' か 'O'、あるいは null を返す。
  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;
}

例えばi = 0出会った場合はconst[a,b,c] = [0,1,2]となります。
つまりsquares[0,1,2]となるわけですね。
で、この[0,1,2]は盤面のマスの位置を示しています。
なのでここまで書けばもうお分かりかと思いますが、

if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c])squares(XかOどちらかのプレイヤー)のマス目が3連になっているかの判定ということになります。
const linesはつまり、縦横斜めで3連になる組み合わせを表していたということです。

次にこれを呼び出して勝利判定を行うようにします。
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');
    }
}

最後に、handleClickメソッドを書き換えてゲームの決着がすでについている場合もしくは、クリックされたマス目が埋まっている場合にreturnするような処理を実装します。


 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,
    });
  }


これで三目並べ自体は完成となりました。

タイムトラベル機能の追加

最後に手番を巻き戻せるようにタイムトラベル機能を実装します。
要は過去の手番を保存しておく配列を用意すればいいわけですね。
手番の状態はsquaresの配列で表現されるということは先程確認しました。
チュートリアルから引用すると

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

このような感じで保存しておけるようにしたいということになりますね。
では進めていきます。

### stateのリフトアップ再び

上記を見るとわかると思いますが、historyは手番をすべて管理している状態と言えます。
そして、historyは状態が常に変化していくこともここまでくればわかると思います。
つまり、historyはstateで管理され、そうなると盤面のマスの状態はhistoryから引っ張ってくればいい=propsとしてやり取りするような処理を書けるということになりますね。
なので、Boardよりさらに上位のGameコンポーネントを作り、そこにstateをリフトアップしていくというのが今回やることです。

では、作業していきましょう。
まずはコンストラクタの移行ですね。


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

  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}


次にGameコンポーネントのrender()をhistoryが使われるように変更します。
Boardコンポーネントからrender()のうちゲームテキスト表示の処理をこちらに移植します。
また、それに伴いhandleClick()も同時に移植してしまいます。


  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = this.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() {
    // historyを引っ張ってくる
    const history = this.state.history;
    // historyに保存されているsquares配列より最新のものを取得
    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コンポーネントをpropsを受け取る形に書き換えます。
やることは

  • コンストラクタの削除
  • this.state.squaresthis.props.squaresへと置き換え
  • this.handleClickthis.props.onClickへと置き換え
  • render()のうち移植した部分を削除

  • handleClick()の削除

こう見るとリフトアップのポイントはどのプロパティをリフトアップするのか(=propsとして受け取ることになるのか)ということを理解することなのかもしれませんね。


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>
    );
  }
}

過去の着手の表示

過去の手番に「ジャンプ」するためのボタンの一覧を作成していきます。
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>
    );
  });

    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()はある配列を別の配列に作り直すことができます。
チュートリアルでは以下のような例で説明されています。


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

historyには手番がsquaresの配列の形ですべて保存されているわけですから、手番を戻す際にhistoryから該当する手番の配列を取得して返したいということになるわけです。
故に上記のようなコードになったわけですが、このままではjumpTo()が未定義であることと、keyの概念が欠如しているのでエラーが出ます。
Keyについてはプロパティにおけるキー、またはデータベースにおけるIDなどと同じような考え方で、Reactではリストのレンダーに関してその履歴に対し個々にユニークIDを持っているわけです。
これはどういうことかというと例えば今回は手番が進むたびに画面が更新されるわけですが、その個々の描画がそれぞれIDを持つということになります。
故にこのIDを用いて、履歴のインデックスとすることが可能というわけですね。
ちなみにKeyはこちらから参照することはできず、Reactから自動的に使用されるようです。

閑話休題、よってjumpTo()の実装とKeyの明示を行わなければいけません。
以下の通りに勧めていきます。

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      // 何手目であるかを表すプロパティを追加
      stepNumber: 0,
      xIsNext: true,
    };
  }

  handleClick(i) {
    // ある手番(A)から過去手番(B)に戻り、そこから新しい手を打った場合、これまでのAまでの手番を参照せずにそれらの履歴を削除する
    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.
    });
  }


  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    // this method has not changed
    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <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>
    );
  }
}


まず、render()の描画の部分に<li key={move}>という形でKeyを明示してやります。
また、何手目の状態であるかを管理するためのstepNumberプロパティを追加しています。
それに伴い、render()current変数を追加し、historyからこのプロパティを用いて現在選択されている手番を取得するように変更します。
次に、そのstepNumberが更新されるように。jumpTo()を定義していきます。
ちなみに先程からいきなりstep及びmoveが引数として登場していますが、コードを見る限りではstepは手番、moveはsquaresの配列を表しているみたいです。
従って、jumpTo()は手番を更新し、かつそれに伴いxIsNextつまりはプレイヤーがどちらであるか設定しているということになります。

ここまで来たら最後にhandleClick()を更新します。
主に巻き戻しを行うのに備えての指定の仕方の変更ですね。
上記のコードにコメントしてありますが、例えばsetState()の部分だとstepNumberのプロパティをstepNumber: history.lengthとしています。
これは先程のjumpTo()が単純にstepを手番として管理していたのに対し、巻き戻しを行った際はそれでは管理できないのでhistory.lengthから引っ張ってくるということになるわけです。

これで、React公式チュートリアルは完了です。
お疲れ様でした、全体のコードは以下のようになっていると思います。

最終的なコード


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

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(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  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
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0
    });
  }

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

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <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>
    );
  }
}

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

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

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;
}



6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2