React

【入門】Reactチュートリアルをやりながら実践入門

業務で開発しているプロジェクトでReactを入れることになりまして、そのためのインプットとしてReactの公式チュートリアルを行いました。
今回はチュートリアルで学んだことを整理しつつ1つ1つの工程ごとにまとめていこうと思います。

Reactの知識が何もない初心者の方でもわかってもらえるように、押さえておくべき部分が出てきた時にその都度解説を挟むようにしています。

Reactチュートリアルとは

そもそもReactはFacebookが開発したもので、公式ページにチュートリアルがあります。こちらにある内容と同じものを今回作成していきました。英語で書かれていますが、こちらでは日本語でまとめていこうと思います。

開発環境

Reactの環境はすごく簡単に作成することができて、やり方に関してはこちらの記事を参考にしてください。

create-react-appコマンドでReactの環境構築を一瞬で作成する今回業務で開発しているプロジェクトにReactを入れてみることになりました。 それにあたって、学習のためにReactのサンプルプログラム...

今回みたいにチュートリアルだけ手軽にやってみたい、というような場合には環境構築をしなくてもhtmlファイルに直接書いていくのでも大丈夫です。
その場合は、


<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Page Title</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  //ここから
  <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/[email protected]/babel.min.js"></script>
  //ここまでを追加
</head>
<body>
  <div id="container"></div>
  <script type="text/babel"> // text/babelにする

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

このようにして書いていけば、ブラウザ上でちゃんと表示されると思います。
ファイル分割をしようと思うとうまくいかないので、分割してしっかり書きたい場合は開発環境を整えていきましょう。

作成するもの

作成するのはこのようなものです。

誰でもやってことがある「○×ゲーム」です。公式ページではtic-tac-toeゲーム(三目並べ)と呼ばれています。

機能としては

  • プレイヤーが交互に順番が回ってくる
  • すでに埋まったマスは埋めれない
  • 1列揃った時点で終了
  • 上に現状のプレイヤーが誰か、勝負がついた場合に勝者が通知される
  • 右に履歴がでてきてボタンを押したらその時点に遡ることができる。

というようなものを作成していきます。

本家の完成版がこちらにのっています。

https://github.com/fkt1993/react-sample-app
今回作成したものはこちらのリポジトリにまとめましたのでそちらでも確認ができます。

初期設定

ある程度出来上がったコードが用意してあって、それをいじっていくことになります。
HTML,cssの部分は完成されているのでjavascriptの部分をひたすらいじっていきます。

本家の最初のコードはこちらです。
https://github.com/fkt1993/react-sample-appでも、コミットを遡れば最初の状態のものがあるので参照することができます。
これ以降の手順を1つずつコミットしてあるので段階ごとに遡れるようにしています。


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('container')
);

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

このようになっています。
簡単に説明していきます。

まずhtmlcontainerというiddiv要素があるのでそこに対して、


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

こう書くことによってcontainerGameコンポーネントでレンダリングされたDOMが入ります。
コンポーネントというのはクラスで書かれていて、renderメソッドを定義すれば先程のというように書けばこの部分にrenderメソッドで返されるDOMがレンダリングします。

あとで説明しますが、stateとして値を保持することもできます。

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

先ほど説明したように、<Game />の部分にはrenderメソッド内に書かれているDOMが返されます。
よく見てみるとこの中身は見慣れたものかと思いきや少し違う部分があるかと思います。
これはjsxといってこれもfacebookが開発した独自タグの技術です。
普段DOMjavascriptで定義するとき文字列の連結を駆使してやっているとわりと見にくかったりします。jsxの表記だとhtmlに近い書き方なので見やすいですがいくつかお作法があります。
その1つがclassNameです。classjavascriptの予約後なのでhtmlのクラスを定義する際にjsxの中ではclassNameと記述します。
もうひとつ、変数を埋め込む際には{}で囲むだけで大丈夫です。これによって変数を埋め込む場合でも見やすいです。{/* 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のマスをレンダリングしています。
renderメソッド内を見ていくと、


{this.renderSquare(0)}

というようになっています。
先程も説明しましたがjsxで、変数を埋め込む場合の表記は{}で囲むだけです。
ここでは、renderメソッド内でstatusを定義してそれを{status}と書いて埋め込んでいます。

また、Boardコンポーネント内にrenderSquareというメソッドが定義されていてそれを呼び出す際には、this.(メソッド名)で呼び出すことができます。
引数も渡すことができます(やりかたは後述)。

renderSquareメソッドを見ていくと、<Square />とあるのでSquareコンポーネントを見ていきます。
これはマス1つをレンダリングするものですね。


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

このようになっています。
ここは先程までの説明と同様ですね。

これで全体像が見えました。
Game,Board,Squareの3つのコンポーネントで構成されていて、しっかりとわけられていて、1つ1つがすっきりしています。
これがReactのメリットの1つです。
こうすることで機能が増えてきても役割が分かれているのでカオスになりにくいです。

チュートリアル

propsを通してデータを渡す

まず手始めにマスの中に何かしらを表示していきます。

現状このように枠しか無いのを

このように数字を順番に表示させていきます。

手順の流れとしてはBoardSquareに対してn番と表示されたマスを表示するという命令を出すようにします。
なのでBoardからSquareに引数を渡すようにします。


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

このように記述することで引数を渡すことができます。


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

受け取る側はこのようにすることで値を受け取ることができます。

これでマスの中に数字が表示されるはずです。

stateで状態をもたせる

次に、このままでは無いも変化がないのでなにもないマスをクリックしたときにXが表示されるようにします。
その場合2つのポイントがあります。

  • 現在のマスの状態をどこかに保持する
  • クリックしたときのどうするかを実装する

です。
まず状態を保持できるようにしていきます。
Componentではstateを使って状態を保持できると先程書きました。それを今回実装していきます。
1つ1つのマスで自分自身のマスに何が表示されているかを保持するようにしていきます。なのでSquareコンポーネントを書き換えていきます。


class Square extends React.Component {
  //constructorを追加
  constructor() {
    super();
    this.state = {
      value: null,
    };
  }
  render() {
    return (
      <button className="square">
        {this.state.value} //props→stateに変更
      </button>
    );
  }
}

stateを実装する場合constructorを定義してこのように書いていきます。
constructorでは明示的にsuperメソッドを呼び出す必要があります。

先程this.props.valueとしていた部分をthis.state.valueとすることでstateの値を表示するようにできます。

次にクリックしたときの挙動を実装していきます。

まずクリックしたときの挙動は


<button className="square" onClick={() => alert('click')}>
  {this.props.value}
</button>

というように記述することで定義できます。
これはJavaScript(ES2015)のアローファンクション構文を使用しています。
今回はマスをクリックするとアラートが表示されます。

今回はマスをクリックした際にstatevaluenullからXに変更したいです。
stateを変更する場合は


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

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

なので最終的に


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

というようにします。

実行してみると、このようになっているはずです。

状態を上に渡す

さて、stateをもたせることができましたが、この先に×を交互にでるようにしたり、勝敗が決まったかどうかを判定したりする機能を作る際に、現状のように各マスがstateを保持している状態では管理がし辛いです。毎回各マスに問い合わせるというやりかたもできなくはないですが、理解しづらい破綻しやすいコードになってしまうのでReactではそうさせないようにしています。

結論としては、上の階層であるBoardコンポーネントに状態をもたせるようにしたほうがいいです。
Reactでは親子のコンポーネントでデータをやり取りする場合には親に状態をもたせるようにするのがセオリーです。

ということでBoardコンポーネントstateをセットしていきます。この際9マスの情報を集めるので9つの要素をもった配列で状態を表すようにします。

加えて、マスがクリックされたときにstateも変化するようにしないといけません。
この場合、onclickで実際に呼ぶ場所は子(Square)、値を保持しているのが親(Board)ということになるのでどのようにすればいいかというと、呼ばれたら状態を変更するという処理を親(Board)で定義して、それを子(Square)に渡すということをします。

なのでBoardはこのようになります。


class Board extends React.Component {
  //constructorを追加
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
    };
  }
  //handleClickを追加
  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)} />; //squareの対応する要素を渡す,onClickを追加
  }
  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 {
  //constructorを追加
  constructor() {
    super();
    this.state = {
      value: null,
    };
  }
  render() {
    return (
      <button className="square" onClick={() => this.props.onClick()}> //state→propsに変更
        {this.props.value} //state→propsに変更
      </button>
    );
  }
}

これで変更が完了です。機能的には特に変わってはいないです。

なぜ普遍性が重要か

さてここで先程の変更で


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

このような部分がありました。
クリックしたときにsquaresの対応する要素を変更するメソッドですが、squaresに変更を加えるのではなくsquares配列をコピーしています。
直接変更してしまえばすぐ終わりな作業をわざわざこのようにしているのは意味があります。

そもそもプログラミングにおいて変数を極力使わずに定数のみを使ったほうがいいというセオリーがあります。というのは変更を許可すると思わぬところで値が書き換わっている可能性があるのでバグを生みやすいのとコードが複雑になって理解しにくなるからです。

ここで、Reactでは、stateの変更を検知してstateを使用している各要素に変更を伝えて、値を受け取り、要素単位で考えたときに受け取った値が変更があれば再レンダリングをするという流れです。このために最小限の部分しか変更しないので描画が軽いという特徴があります。

もし変更が許可されたオブジェクトの変更を検知する場合には、全体のオブジェクトツリーをスキャンしてそれぞれの変数と値を比較する必要があります。これは複雑かつ時間がかかります。
一方変更が禁止されているオブジェクトでは参照されているオブジェクトが以前のものと同じかどうかを判定するだけでいいので変更の検知がしやすいです。

したがってReactの特徴を最大限いかすために普遍性が重要になるわけです。

関数コンポーネント

Squareコンポーネントstateを持たなくなったのでconstructorは不要になったので削除します。
その場合Squareコンポーネントrenderメソッドのみからなるコンポーネントとなります。Reactではこのようにrenderメソッドのみからなるコンポーネントはfunctional componentsとして書くことができます。


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

Squareコンポーネントはこのように書くことができます。
この場合this.propsと今までしてたのが、propsとなるのに注意です。

○と×が交代する機能を作成

現状だとXしか表示されないのでXが交代になるようにしていきます。
stateに次のプレイヤーがどちらになるかを保持するものを追加します。


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[i] = this.state.xIsNext ? 'X' : 'O'; //プレイヤーで分岐
    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>
    );
  }
}

このようになったかと思います。

勝敗を判定する

次は勝負がついてきたときの表示を実装していきます。
勝敗がついたかどうかを判定する関数自体はもともと用意してくれていて、calculateWinner(squares)が下の部分に実装されているかと思います。
reactでサーバーを建てて実行している場合はwarningで今までcalculateWinnerを使ってないよっていうのがでててかと思いますがここから使用します。

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();
    //追加
    if (calculateWinner(squares) || squares[i]) { //勝敗がつくかすでにマスが埋まっているときになにもしないようにする
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }
  renderSquare(i) {
    return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
  }
  render() {
    //ここから追加
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      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>
    );
  }
}

これでちゃんと動くものが完成しました!このようになっているかと思います。

履歴を記録する

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

どのようにしていくかというと、すべてのマスの状態を配列として格納していましたが、毎回動作のたびに新しい配列を作成していたのを利用します。
historyという配列を作成して、その1つの要素に今までのsquaresを格納するようにします。そうすることで各動作ごとの状態を保持することができます。

そして、各動作のリストをどこで表示させるかを考えていきます。Boardコンポーネントでは3×3のボードを作るのに専念させたほうがいいのでよりトップレベルのGameコンポーネントにその役割を任せます。
そうした場合に、序盤でSquareコンポーネントからBoardコンポーネントに状態を引き上げたように、今回もBoardコンポーネントからGameコンポーネントに状態を引き上げていかないといけません。

Boardはこのようにconstructorを削除して、stateを参照していた部分をpropsに変更します。
statusを表示していた部分はGameがその役割を担うので削除します。


class Board extends React.Component {
  renderSquare(i) {
    return <Square value={this.props.squares[i]} onClick={() => this.props.handleClick(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コンポーネントは以下のようになります。


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

いままでrenderメソッドしかなかったのをすべてここに集約するようになっています。
役割を移しただけなのでまだ機能としては変わっていません。

ここから履歴を表示するようにします。


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

    //追加
    const moves = history.map((step, move) => {
      const desc = move ?
        'Move #' + move :
        'Game start';
      return (
        <li>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </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> //movesに変更
        </div>
      </div>
    );
  }
}

このように変更します。
Reactで複数のアイテムをレンダリングする際の一般的な方法は、配列のデータをmapすることです。
map関数DOMを返すようにしていきます。

これを実行するとこのようにwarningが出てきてしまいます。

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

Key

Reactではレンダリングを最小限にするために変更されたものしか再レンダリングをしないような仕組みになっています。
先程のwarningの原因は、リストを再レンダリングを最小限にするための仕組みがどうなっているかを考えてみると見えてきます。

このようなリストがあったとします。


<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>

それがこのように変更したとします。


<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>

人間の目で見た際には順番が変わって、1つ要素が増えて、中身の値も一部が変わっているというようにわかりますが、Reactはこれだけでは今のようなことを認識してくれるわけではありません。
そこでReactではリストの各要素にkeyプロパティを指定することで順番が変わったとしても新しく追加されたとしても、中身の値を変更する必要があるかどうか認識することができます。
認識するためにはkeyはユニークである必要があるので今回は、alexa,ben,claudiakeyとするのがよいです。上から順番に1からふっていくというようなやり方は順番が変わったとしても上から順番に1から振られてしまうので適切ではありません。DBのオブジェクトと一致する場合はDBIDkeyとして使うのが適切です。

このようにReactではアイテムのリストをレンダリングする際に常にリスト内の各アイテムの情報を記録しているので、要素数が多かったとしても再レンダリングする必要があるアイテムだけ再レンダリングされます。

マルバツゲームの実装に戻ると、このwarningを解消するためには今までの話を踏まえるとkeyを要素に追加すればいいわけです。


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

これでwarningが解消されたかと思います。

jumpToを実装

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


class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }], //squaresを要素に持つ配列にする
      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>
    );
  }
}

これですべて完成です!
完成品はこの様になっています。

コード類はhttps://github.com/fkt1993/react-sample-appここで管理もしているので良かったら参考にしてください。

追加課題

チュートリアルの内容は一通り完成しましたが、一番最後に以下のように追加課題が課せられています。

1. それぞれの遷移に(col,row)の表示をする
2. 遷移リストの現在選択されているものを太字に
3. Boardをloop2回つかって書いてみる
4. 遷移リストを昇順/降順にする
5. 誰かが勝った時に、3つ揃ったセルをハイライトする
6. 引き分けの時にはdrawを表示

公式のチュートリアルでは追加課題は課題が課されるだけで実際の答えのようなものは存在しません。

こちらの記事で、追加課題をやってみたものをまとめてみたので、もし興味があれば見てみてください。

【入門】Reactチュートリアルをやりながら実践入門〜追加課題編〜Reactの学習用に開発元のFacebookが公式のチュートリアルを提供してくれています。そのチュートリアルの内容の一番最後に追加課題が出されていて、今回はその追加課題をやってみたものを初心者でもわかるようにコードも載せつつ解説していきます。...

おわりに

今回React.jsのチュートリアルを行いました。内容を見ていくと単なるチュートリアルだけではなく概念など細かい部分にも言及されていたので、理解のために非常に助けになりました。

 

created by Rinker
¥400
(2024/03/29 10:37:16時点 Amazon調べ-詳細)