LoginSignup
0
2

More than 3 years have passed since last update.

【React】チュートリアルの三目並べをやる #1

Last updated at Posted at 2020-03-22

前回まではProgateの無料レッスンを一通りこなしました。

今回からはアプリを作成するアウトプットを行っていこうと思います。
または、自分が新たに習得した知識の共有等も行えたら良いなと思います。

次回
【React】チュートリアルの三目並べをやる #2

React公式チュートリアル

Reactの公式チュートリアルに三目並べゲームがあり、なぜかこれが気になってしまうので、作っていきたいと思います。

最終成果物は → 三目並べゲーム

これをチュートリアルに沿って作るのが目標です。

前提知識

アロー関数、クラス、letおよびconstが理解できていることが前提らしい。
完璧には理解できていないが、わからなければググるので問題なし。

チュートリアルの準備

開発環境は以下の2つ

  • ブラウザで書く
  • ローカルに開発環境を構築して書く

以前Progateの無料レッスンをやっているので、ローカルに構築済みです。
Progate無料版をやってみる【React】

以下のソースコードを元に作成する模様。
https://codepen.io/gaearon/pen/oWWQNa?editors=0010

イメージ
image.png

3つのコンポーネントで構成される

  • Square: マス目を表し、buttonタグをレンダー
  • Board: 9個Squareをreturnしている。マス目全体
  • Game: あれ、これ何を表しているんだ?  チュートリアルには「後ほど埋めることになるプレースホルダーを描画しています」とのこと。後ほど書くのね。

とりあえず元となるソースをコピペまたは模写していきます。
index.jsを変更しちゃいます。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorker from './serviceWorker';

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 palayer: 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="gmae-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <div>{/* TODO */}</div>
        </div>
      </div>
    );
  }
}

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

一番下のserviceWorker.unregister();を消すとエラーになっていた。
以前のインストール時のHello World的なチュートリアルで入ってしまった模様。
キャッシュらしいです。よくわからない・・・。
参考にさせていただきました。

index.css
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;
}

Propsでデータを渡す

Props(properties)を用いて親から子に値を受け渡す。
Board(親)からSquare(子)に値を受け渡します。
渡す値は、Board.renderSquareの引数iです。

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

プロジェクトトップに移動し(cd 移動先)

npm start

します。

そしてhttp://localhost:3000にアクセスします。
image.png
おお!でた。
Boardから数字が渡ってきてマスの中に表示されているんですね。

マス目をクリックされたときのイベントを設定する

jQueryだと

$('button.square').click(() => {
  // 何か処理
});

素のJavaScriptだと

document.querySelectorAll('button.square').forEach((node) => {
  node.addEventListener('click', () => {
      // 何か処理
  });
});

となり、HTMLとは別個に記述します。

JSXの場合HTMLと一緒に書くようです。

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

た、多分JSXのほうがイイヨネ・・・。
今はこの独特の書き方が慣れてないんで微妙ですが、たくさん書けば良さを実感できるんでしょうか。

マス目の中身をXに変更する処理は、jQueryや素のJavaScriptのようにDOMAPIを呼ぶ書き方ではなく、stateを用いる方法で実現します。(コンパイル後には結局DOMAPIを呼ぶ書き方に変わりますが)

Square
constructorを追加し、引数にpropsを設ける
super(props)で継承元のコンストラクターを呼ぶ。お決まり
statevalueプロパティを持つオブジェクトを設定。valueプロパティの値はprops.valueを設定
onClick内ではsetStateする
・レンダー時のマス目の値はprops.valueからstate.valueに変更する

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

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

結果
ダウンロード (1).gif
ブラウザで確認時、Reactのコンポーネントツリーを調べる拡張機能があるらしい。
React Devtools 拡張機能

マス目の制御を行う

現在のマス目の内容の状態を取得するには、Board側から各Squareに取りに行くイメージでやればいいと思いがちですが、可読性が落ちたり、バグが起きやすいとのことです。
Board側で各Square側の状態を保持しておいて、propsを用いてやり取りするのがベストらしいです。

具体的にはまず、Board側にSquareに渡す値の情報を保持します。
→ マス目情報を長さ9つの配列として保持します。

Boad側

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

Array.prototype.fillの説明

renderSquareメソッドで配列のi番目を渡すようにします。

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

そしてSquareからBoardを更新してもらうために、Squareに対して更新用の関数を渡してあげます。
上のvalue={this.state.squares[i]}に続いてonClickを定義。

Board
renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]} // クロージャでiが保持されている
        onClick={() => {
          const squares = Object.create(this.state.squares); // 別の配列としてクローン
          squares[i] = 'X';
          this.setState({ squares: squares });
        }}
      />
    );
  }

更新処理が書かれたこの関数ごとSquareに渡るイメージです。
Square側のpropsに入っているイメージ。

iについて
renderSquare(i)のところのiはクロージャによって、最初にレンダーされた時の各Squareごとの値を保持しています。
クロージャ

onClick内の更新処理について
Reactのstateは直接更新は推奨されていないので、一度Object.createでコピーを作ってから、そのコピーを書き換えて、setSateで更新しています。
なぜ推奨されないか

チュートリアルではコピーのところはthis.state.squares.slice()でしたが、これは配列の中身がプリミティブ値(数値、文字列、真偽値)の1次元配列の時のみ有効です。配列の中にオブジェクトや、さらに配列が入っていたら、完全なコピーができません。(ディープコピー)

コピー元の配列(state)と同じメモリの場所を参照してしまい、この後これに変更を行ってしまうと、「元のstateを変更しない」に反します。
このチュートリアルでは1次元の数値の配列なのでOKですが、今後それ以外のケースが出てきたときに誤ってsliceを使わないようにObject.createで慣れておこうと思います。
Array.prototype.slice()

Square側

・onClickの中で自身のstateを更新していたのをBoardから渡ってきたpropsのonClickを呼ぶように変更します。
・さらに、マス目に表示していたthis.state.valuethis.props.valueに変更します。
 これを行うことにより、ボタン押下時のBoard側で変更されたstateの値がこのpropsに反映されます。
・stateを持つ必要がなくなったので、constructorを削除します。

Square
class Square extends React.Component {

  // constructorは不要になったので削除

  render() {
    return (
      <button
        className="square"
        onClick={() => {
          this.props.onClick(); // BoardのonClickを呼び出す。
        }}
      >
        {this.props.value} // state → propsに変更
      </button>
    );
  }
}

ここまでのBoardとSquareの全文

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

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]}
        onClick={() => {
          const squares = Object.create(this.state.squares);
          squares[i] = 'X';
          this.setState({ squares: squares });
        }}
      />
    );
  }

  render() {
    const status = 'Next palayer: 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はstateを持たなくてよくなったので、React.Componentを継承する必要もなくなります。
もっと簡潔に書けるようになるみたいです。

Square
function Square(props) { // 継承がなくなった
  return (
    // onClickのところがシンプルに
    // あとpropsにthisがいらない。引数で来ているので
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

手番の処理(XやOを実装)

先手はXにします。
Boardに「次の手番はだれか」を判定するためのフラグを設けます。
→「次の手番はXだ」というフラグを設けて、その値のtrue falseで判定します。
その判定により、XOを代入します。

Board

Board
class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true, // 先手はXなので初期値true
    };
  }

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

          // xIsNextがtrueならX、それ以外なら○
          squares[i] = this.state.xIsNext ? 'X' : 'O';

          // xIsNextは現在の反対を代入することで手番が変わる
          this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
        }}
      />
    );
  }

ついでに画面に表示していた文字のところのNext palayer:も動的に書き換わるようにします。

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

あれ?でもこれここのstatusを書き換えて意味あるの?って思っちゃいました。
なぜなら動的に変更するものはstateに入れておかないとダメとおもっていたので・・・。
このrenderがマス目を毎回クリックするたびに毎回動くなら大丈夫ですけど・・・。

→ 確認したら毎回動いてました・・・。
でも毎回動くのって無駄なような気が・・・。
毎回このhtmlを作成しているってことですよね。
このチュートリアルは要素数が少ないからいいですけど、普通のwebアプリを作ろうとしたらめちゃめちゃ要素数多いページとかあるのにレスポンス大丈夫なんでしょうか・・・。
後日調べたいと思います。

ゲーム勝者の判定

動かしてみると分かりますが、今のままだとゲームが終わった後に何度でも入力が出来てしまいます。
なので、勝敗が着いたら何もさせないような制御を実装します。
また、どっちが勝ったかを分かり易くする為に、「勝者: ○○」の文字を表示させようと思います。

チュートリアルでは勝敗を判定してくれる関数を用意してくれています。これをコピーして貼り付けます。

チュートリアルではBoardの外に書いてますが、私は中に入れました。(renderの下)

Board
// 勝敗判定関数
  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;
  }

勝者を表示させるNext palayer:の部分を変更します。

Board
render() {
    const winner = this.calculateWinner(this.state.squares);

    let status;
    if (winner) {
      status = '勝者:' + winner;
    } else {
      status = '次の手番: ' + (this.state.xIsNext ? 'X' : 'O');
    }

最後に

  • マス目が押された時に勝敗が決まっていたら
  • 既にマス目が埋まっていたら

何も処理しないようにします。

Board
renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => {
          const squares = Object.create(this.state.squares);
          if (this.calculateWinner(squares) || squares[i]) {
            // 何もせずにreturn
            return;
          }

          squares[i] = this.state.xIsNext ? 'X' : 'O';
          this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
        }}
      />
    );
  }

ifのところは
左の式は、this.calculateWinner(squares)の戻り値がnull以外なら勝敗が決したということです。
右の式は、suare[i]null以外なら既にマス目に値が入っているということです。

以下、完成した全文

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorker from './serviceWorker';

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

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

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => {
          const squares = Object.create(this.state.squares);
          if (this.calculateWinner(squares) || squares[i]) {
            return;
          }

          squares[i] = this.state.xIsNext ? 'X' : 'O';
          this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
        }}
      />
    );
  }

  render() {
    const winner = this.calculateWinner(this.state.squares);

    let status;
    if (winner) {
      status = '勝者:' + winner;
    } else {
      status = '次の手番: ' + (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) {
    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;
  }
}

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

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

動作結果
ダウンロード (1).gif

おわりに

ここまでで一旦、三目並べゲームが完成です。
Qiitaに書きながらなのでなかなか苦労しました・・・汗

チュートリアルではこの後「タイムトラベル機能」の追加の手順があるので、次回はそちらをやっていこうと思います。
履歴から手順を戻すやつですね。

GitHub・・・公式にソースあるしいらないかw

→ 次回

0
2
7

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
0
2