Help us understand the problem. What is going on with this article?

普段Rails触ってる人がReactチュートリアルやってみた

More than 1 year has passed since last update.

これは

  • 業務でRails(Ruby)を書いている人がフロントの勉強としてReactチュートリアルをやってみたMEMOです
  • 書き方悪いところやリファクタリングできるところが山ほどあると思いますが、多めに見てくださるか優しくご指摘頂けると助かります。。(間違っているところは容赦無くお願いします)

この時のわたし

  • Rails(Ruby)でバックエンドの実装に携わる
  • 業務でフロント連携することが多く、自分でもさわれたらいいなーと思った
  • とりあえずJS(ES6)の基礎の基礎だけ学んだ
  • Rubyの他にちゃんと触ったことある言語はHTML・CSS・JS(jQuery)くらい

リポジトリ

https://github.com/hak-chami/react_tutorial

Reactチュートリアルとは

  • Reactの公式チュートリアル
  • Reactだけで三目並べ(いわゆるマルバツゲーム)を作れる
  • 今現在日本語版もできており大変ありがたい

公式の前提知識という項目に

  • HTML と JavaScript に多少慣れていることを想定
  • 関数、オブジェクト、配列、あるいは(相対的には重要ではありませんが)クラスといったプログラミングにおける概念について、馴染みがあることを想定
  • また ES6 という JavaScript の最近のバージョンからいくつかの機能を使用していることにも注意

とあるので、その辺りについて軽く予習した方が効率が良いかと思います

チュートリアルの準備

  • チュートリアルを始める前にという項目があるので、それに沿って準備をする
  • 手順や説明など詳しくチュートリアルに書いてある部分は割愛
  • 詰まったところやわからなくて調べたところをメインに残している

チュートリアル:React の導入 – React

  1. ブラウザでコードを書く
  2. ローカルの開発環境で作る

どちらか選べますが、わたしはローカルの開発環境で作る方で進めました

新しい React アプリを作る – React

  • npx?
    • npm 5.2 から利用できるパッケージランナーツール
  • npm?
    • Node Package Manager
    • Nodeを管理するツール

ローカルで開発できるようにし、さらに手順に沿って進めると

image.png

  • 三目並べ用の枠ができた!

この状態でRailsと同じようにターミナルから

nps start

とするとlocalhost:3001でブラウザで表示できるようになります

概要

これで準備が終わったので次は概要を学んでいきます

React とは?

  • React?
    • JavaScriptのライブラリ
    • コンポーネントと呼ばれる部品を組み合わせて複雑なUIを実現できる
  • props = properties の略
  • renderで表示するビューの階層(説明書き)を返す
  • Reactがその説明を読み取って画面に描画する
  • BabelES6->ES5のトランスパイラ

スターターコードの中身を確認する

最初に作ったindex.jsを見てみるとReact.Componentを継承したクラスが3つありました

  • extendsで継承(ES6)
  • ex) SquareクラスがReact.Componentクラスを継承(extends)している
class Square extends React.Component

image.png

  • Gameクラスはこの時点でまだ実装されていない

データを Props 経由で渡す

チュートリアル通りにコードを書き換えていくとこんな感じで表示されるようになりました

image.png

  • React では、親から子へとpropsを渡すことで、アプリ内を情報が流れていく
  • ここではBoard(親)-> Square(子)へpropsとしてvalueという名前の値をSquareに渡している

image.png

image.png

インタラクティブなコンポーネントを作る

this の混乱しやすい挙動を回避するため、この例以降ではアロー関数構文をつかってイベントハンドラを記述

  • アロー関数は親のthisを継承する(ES6)
  • onClickプロパティに渡しているのは関数なのでReactはクリックされるまでこの関数を実行しない

これでクリックされた時のイベントを作れたので今度はクリックされたらXが表示されるようにします

  • Reactコンポーネントはコンストラクタでthis.stateを設定することで、状態を持つことができるようになる
  • 現在のSquareの状態をthis.stateに保存して、マス目がクリックされた時にそれを変更することができる

Squareconstructer内でthis.stateとしてvalue: nullを定義し、
renderする時に渡されたpropsではなくthis.stateとすることで初期値のvalue: nullが反映され↓の状態に戻りました!

image.png

クリックイベントでalertを表示していたところを

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

とすることでstateの値をXに更新して表示できるようになりました!

image.png

  • Squarerenderメソッド内に書かれたonClickハンドラ内でthis.setStateを呼び出すことで、Reactに <button> がクリックされたら常に再レンダーするよう伝えているため
  • setStateをコンポーネント内で呼び出すと、React はその内部の子コンポーネントも自動的に更新する

Developer Tools

  • ChromeにReactのDeveloperツールを入れよう
  • 下記リンクからインストールできる

React Developer Tools - Chrome ウェブストア

image.png

ゲームを完成させる

ここからゲームにしていくために下記を実装してきます

完全に動作するゲームにするためには、盤面に “X” と “O” を交互に置けるようにすることと、どちらのプレーヤが勝利したか判定できるようにすることが必要

State のリフトアップ

  • 親コンポーネント内で共通のstateを宣言することで子コンポーネント同士を兄弟のようにでき、互いにやりとりできるようになる
  • 親コンポーネントはpropsを使って子に情報を返せる(ここではvalue={i};の部分)
    • こうすることで子コンポーネント同士、親子同士で常に同期されるようになる
this.state = {
      squares: Array(9).fill(null),
    };

// Array(9) -> 9この要素を持った配列を作る, fill(null) -> 配列の要素を全てnullで埋める

【JavaScript】配列を同じ値で埋めたり、連続した値を設定する - Qiita

こうするとBoardstateとして

[
  X, O, null,
  X, null, O,
  null, X, null
]

みたいな配列を持つことになります

  • ここを更新してその値を子に伝えることで子のstateを管理することができる
  • 子がクリックされた時に親のstateを変更したいが、できないので親の関数を呼んでstateを更新するようにする

これで子が自身のstateを管理しなくなったので子のconstructorはいらなくなりました

image.png

handleClick()を定義していないと怒られたので定義し、クリックされたらsquaresの値を更新してやるとまたXが表示されるように!

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

// arr.slice(begin[, end]); -> beginを省略するとindex0からになる

.slice() | JavaScript 日本語リファレンス | js STUDIO

image.png

イミュータビリティは何故重要なのか

  • イミュータブルなオブジェクトは変更が簡単
  • 参照しているイミュータブルなオブジェクトが前と別のものであれば、変更があったということ
  • Reactにおいてはコンポーネントをいつ再renderすべきなのか決定しやすくなる

関数コンポーネント

  • 関数コンポーネント
    • renderメソッドだけを有して自分のstateを持たないコンポーネントを、よりシンプルに書くための方法

Squareクラスは自分でstateの管理をしなくなってconstructorを削除したので関数コンポーネントに置き換えられます

手番の処理

 squares[i] = this.state.xIsNext ? 'X' : 'O';

this.state.xIsNexttrueorfalseで値を持っており、クリックイベントでstateの真偽値を反転させます
そしてtrueならXをfalseならOにsquares[i] を更新するようにします

constructor(props) {
  super(props);
  this.state = {
    squares: Array(9).fill(null),
    xIsNext: true, // ここで次の手がOかXかの状態を管理
  };
}

handleClick(i) {
  const squares = this.state.squares.slice();
  squares[i] = this.state.xIsNext ? 'X' : 'O';
  this.setState({
    squares: squares,
    xIsNext: !this.state.xIsNext, // クリックしたら真偽値を反転させてstateを更新する
  });
}

ゲーム勝者の判定

勝ち負け判定ができる関数が用意されているのでそれを使用します

  • 勝ったプレイヤーがいたらXOを、いなかったらnullが返る
render() {
  const winner = calculateWinner(this.state.squares);
  let status;
  // winnerがnullじゃなかったらどちらかが勝ちで X or O が返ってきている
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
  }

また、handleClick(i)内に下記を追加することで「決着がついている」「すでに盤面が埋まっている」場合、クリックでXorOの更新をしないようになっています

if (calculateWinner(squares) || squares[i]) {
    return;
  }

最初、なんでsquares[i]?って思ってましたが、すでにXが入っているところがOに更新されたらおかしいですもんね
よく考えたら当たり前のことでした…

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

ゲームの進行状況を巻き戻したり、盤面をリセットしたりする機能を追加していきます

着手の履歴の保存

squaresの配列を着手があるたびhistoryという名前の別の配列に保存していくことで履歴として保持することができます

State のリフトアップ、再び

Gameクラスで全ての値を管理します

クリックイベントは
- squareでどの盤面がクリックされたか判断する
- Boardからクリックされた盤面の情報をGameに伝える
- GameのhandleClickで値を更新する
- handleClickで盤面の情報をhistoryに追加して記録していく

image.png

過去の着手の表示

// mapの第二引数はindex(何回目か)
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>
  );
});

Array.prototype.map() - JavaScript | MDN - Mozilla

key を選ぶ

リストをrenderする際Reactはkeyで差分を測る(key = Reactの予約語)

<li key=重複しない値>リストの中身</li>
<!-- コンポーネントとその兄弟の間で一意であればOK(グローバルで一意の必要はない) -->
  • もし以前になかったkeyがリストに含まれていれば、Reactはコンポーネントを作成する
  • もし以前のリストにあったkeyが新しいリストに含まれていなければ、Reactは以前のコンポーネントを破棄する
  • もし2つのkeyがマッチした場合、対応するコンポーネントは移動する
  • keyはそれぞれのコンポーネントの同一性に関する情報をReactに与え、それによりReactは再レンダー間でstate を保持できるようになる
  • もしコンポーネントのkeyが変化していれば、コンポーネントは破棄されて新しいstateで再作成される
  • コンポーネントが自身のkeyについて確認する方法はない

以上のことから動的なリストを構築する場合は正しいkeyを割り当てることが強く推奨される

const history = this.state.history.slice(0, this.state.stepNumber + 1);
// array.slice(begin, end); -> 第二引数は抜き出す要素の終わりを指定する

これにより任意の範囲でhistoryを取り出す(まきもどした時点以降の履歴を削除する)

Array.prototype.slice() - JavaScript - MDN - Mozilla

拡張課題

ここからが本題!ということでそれぞれ考えながら実装してみました
拡張課題は全部で6つあります

  1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
  2. 着手履歴のリスト中で現在選択されているアイテムをボールドにする。
  3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
  4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
  5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
  6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

着手した盤面のcol,rowを一緒に保存したいというとで、

Go to move #1(col: 1, row: 3)

みたいな感じで表示したいなーと考えました

まずどうやってcolとrowの出すかだけど、ここがめっちゃ悩んだ気がします。。
図を書いて考えて

image.png

// col
i % 3 + 1;
//row
i / 3(の商) + 1;

とりあえず、これで
ということで、rowは商の部分だけ欲しいので整数にするメソッドを探した

実数を整数に丸める4パターン(JavaScript おれおれ Advent Calendar 2011 – 7日目) | Ginpen.com

与えられた値を上回らない最大の整数、つまり同じか小さい整数を返します。

//row
Math.floor(1 / 3) + 1;

次にコードのどこに書くか、ですが
表示するところはmoveのところなので、コードを見てみると
Game内のrender部分でhistroymapして繰り返し表示しているから、historysquaresと共に持たせるのが良さそうと思い
まずhistoryの中にcolrowの情報を持たせました

this.setState({
  history: history.concat([{
    squares: squares,
    // ここにさっき出した計算式を入れた
    col: (i % 3) + 1,
    row: Math.floor(i / 3) + 1,
  }]),
  stepNumber: history.length,
  xIsNext: !this.state.xIsNext,
});

で、render内ではmapで順番に取り出したstepのなかのcolrowなので一緒に表示させてあげます

const moves = history.map((step, move) => {
  const decs = move ?
    // historyで持ってるcolとrowの値を表示する
    `Go to move #${move}(col: ${step.col}, row: ${step.row})`:
    `Go to start`;
  return (
    <li key={move}>
      <button onClick={() => this.jumpTo(move)}>{decs}</button>
    </li>
  );
});

文字列はテンプレートリテラルを使って書き換えてます!(かきやすい!)
これで、できました!!

image.png

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。

着手履歴を表示しているところは上記と同じくGame内のrender部分なので、条件で表示を変えるif文を使えば良さそうです

「現在選択されているアイテム=最新の履歴」と考えて、

  • もしmovehistoryの中身の数だったら
    • 文字をボールドにして表示する
    • そうじゃなかったら普通に表示する

とすれば良さそう!

history.lengthstateの中のstepNumberで定義されているのでそれを使えそうなので、returnの中身を

return (
  <li key={move}>
    <button onClick={() => this.jumpTo(move)}>
      { move === this.state.stepNumber ? <strong>{decs}</strong> : decs }
    </button>
  </li>
);

こんな感じで書き換えました!
条件分岐の処理は複雑じゃないのでインラインで三項演算子を使っています

image.png

3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

Board でマス目を並べる部分は繰り返しが2回あります

  • 1つは<div></div>を3回繰り返すところ
  • もう一つはそのなかで{this.renderSquare(i)}を3回繰り返すところ

mapをネストすればやりたいことができそう
また、チュートリアルの最後に書いてあったように

動的なリストを構築する場合は正しい key を割り当てることが強く推奨される

なのでそれぞれにkeyを渡す必要がありそうです
history表示させているとこもmapindexkeyにしているので、こちらも同じようにしてみます

render() {
  const boardSquares = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
  return (
    <div>
      { 
        boardSquares.map((rowSquares, i) => {
          return(
            <div className="board-row" key={i}>
              {
                rowSquares.map((square) =>{
                  return(
                    this.renderSquare(square)
                  )
                })
              }
            </div>
          )
        })
      }
    </div>
  );
  }
}

これでうまく表示されたけど、ちょっとパワープレイ感あるのでどうにかリファクタしたいですね。。
あとrenderSquareの部分にもkey持たせなかったら怒られてたので追加

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

boardSquaresの部分をいい感じに配列作れるようにできれば…?と思ってちょっとチャレンジしてみようと思い
どうやったらいいかちょっと考えました

  • まず、盤面の一辺のマス目を決める
    • 3
  • 正方形の盤面なので、マス目の数は一辺の2乗
    • 9
  • マス目の数を0から始めてマス目分だけ順番に配列にする
    • [0, 1, 2, 3, 4, 5, 6, 7, 8]
  • その配列を一辺の数ごとに配列でくくる
    • [[0, 1, 2], [3, 4, 5], [6, 7, 8]]

そしてできたのがこちら
ちょっと長くなったけど、boardLinesの数を変えると盤面がいい感じに変わるようになりました!

render() {
  // 一辺が何マスの盤面を作るか
  const boardSideLength = 3;
  // boardSideLengthから盤面のsquareの数を出して0から始まる配列にする
  const boardSquares = [...Array(boardSideLength ** 2).keys()];
  // 配列からrowごとのまとまりを作る
  const boardSquaresList = boardSquares.reduce((square, i) => {
    const lastSquare = square[square.length - 1];
    if (lastSquare.length === boardSideLength) {
      square.push([i]);
      return square;
    }
    lastSquare.push(i);
    return square;
  }, [[]]);
  return (
    <div>
      { 
        boardSquaresList.map((rowSquares, i) => {
          return(
            <div className="board-row" key={i}>
              {
                rowSquares.map((square) =>{
                  return(
                    this.renderSquare(square)
                  )
                })
              }
            </div>
          )
        })
      }
    </div>
  );
  }
}

(Rubyで書くとこんなかなぁと思いつつ)

board_squares = Array(0..(board_side_length ** 2)-1)

そんなわけで盤面を4×4にしたり

image.png

10×10にしてもちゃんとrenderされるようになりました!

image.png

(なお当然だがゲームとしては成り立っていないw)

image.png

勝ち負け判定とcol,rowの計算で3って置いてるのどうにか直したら五目並べ的なゲームにできそう感はあるけど難しそうですね(勝ち負け判定のところが特に)

4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

だんだん考えることが多くなってきたので、順を追いながら一つずつ解決してみることにしました

  • とりあえずボタンを作成したい
  • どこに?
    • Gameの表示部分、statusの下あたり
const orderBottun = `ASC <-> DES`;  // buttonの中身

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>
      <div><button>{orderBottun}</button></div> // buttonを追加
      <ol>{moves}</ol>
    </div>
  </div>
);

image.png

  • ステータスに昇順か降順かの値を持たせる(true/falseで切り替え?)
    • isAsc = true(昇順)/false(降順)
  • どこで?
    • Gamestate
  • 降順での表示方法は?
constructor(props) {
  super(props);
  this.state = {
    history: [{
      squares: Array(9).fill(null),
    }],
    stepNumber: 0,
    xIsNext: true,
    // いったんfalseで置いて降順になるか確認
    isAsc: false,
  };
}
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>
      <div><button>{orderBottun}</button></div>
      <ol>
        { this.state.isAsc ? moves : moves.reverse() } // falseなら逆で並べる
      </ol>
    </div>
  </div>
);

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。 の時とやってることは同じですね

image.png

逆になっていますね!

  • ボタンをクリックしたらisAsctrue/falseが逆になるようにする
  • まずクリックした時のイベントを作る
    • jumpToを参考に押されたらstateを更新する -> isAsctrue/falseの入れ替え
handleOrder() {
  this.setState({
    isAsc: !this.state.isAsc,
  });
}
  • ボタンにonClickイベントを作る
<button onClick={() => this.handleOrder()}>
  {orderBottun}
</button>
  • ついでに今どっちの並び順なのかボタンでわかるようにしてみた
const orderBottun = this.state.isAsc ? '↑OLD NEW↓' : '↑NEW OLD↓';

image.png image.png

どっちが新しい手順なのかボタンでわかるようになりました

5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。

勝った時にどのラインで勝ったのかがわかれば、該当のマスだけ色をつける、といったことができそうです

  • 勝ち判定がでたときに、一緒に該当のラインを返すようにする
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する
    return { player: squares[a], line: [a, b, c] };
  }
}
return null;
  • これで勝ったときに勝ちプレイヤーとラインが入ったObjectが返ってきた

image.png

  • winner表示のところでObjectからplayerを取り出して表示する
const winnerInfo = calculateWinner(current.squares);
let status;
if (winnerInfo) {
  status = `Winner: ${winnerInfo.player}`; 
} else {
  status = `Next player: ${this.state.xIsNext ? 'X' : 'O'}`;
}

これでちゃんと表示されますね

  • 次に、色をつけるところ
  • lineから勝ち判定のマスを引っ張ってきて色をつければいい
  • どこに?
    • Square<button> ... </button>
  • いったんCSSにハイライトの設定を作って反映させてみる
.highlight-color {
  background: #ff0000;
}
function Square(props) {
  return (
    <button className="square highlight-color" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

image.png

できました!

とはいえ、どこからこの情報を持って来ればいい?どういう条件で表示する?ってとろこで少し悩みました

  • データはprops経由で親から子へ渡る
    • チュートリアルを見返すと

Board の renderSquare メソッド内で、props として value という名前の値を Square に渡すようにコードを変更します:

  • もう一回コードをよく見てみると
    • Squareで表示しているのはprops.value
    • こいつはどこの値かというとBoard内の 内のvalue={ this.props.squares[i] }
class Board extends React.Component {
  renderSquare(i) {
    return <Square
              value={ this.props.squares[i] }
              onClick={ () => this.props.onClick(i) }
              key={i}
            />;
  }
 ...
  • じゃあこのthis.props.squares[i]はどこから来てるんだ
  • Game内の 内のsquares={current.squares}っぽい
return (
  <div className="game">
    <div className="game-board">
      <Board
        squares={current.squares}
        onClick={(i) => this.handleClick(i)}
  />
...

コードを読み返してわかったこと:

  • Game -> Board -> Squarepropsが渡ってるので
  • Game内の<Board ... /> -> Board内のrenderSquare(i)(<Square ... />) -> Square<button>と値を渡してやれば良さそう

というわけで

  • 「もし、勝ったラインの配列にSquareiがあったら、色をつける」みたいな条件にするとしたら
    1. Game -> Board へは勝ったラインの配列
    2. Board内のrenderSquare(i)で渡ってきた配列にiが含まれているか(true/false)を
    3. Squareで渡って来た値がtrueだったら色をつける、とする

という方針で進めることにしました

1. Game -> Board勝ったラインの配列を渡す

  • 勝ち判定をif (winnerInfo) { 〜の部分でしているので、勝った場合winnerInfo.lineを入れる部分を作ってやる
let status;
let winLine = []; // 追加
if (winnerInfo) {
  status = `Winner: ${winnerInfo.player}`; 
  winLine = winnerInfo.line; // 勝ち判定が出た場合ここに勝ちにつながったlineを入れる 
} 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)}
        winLine={winLine} // ここでpropsとしてBoardにwinLineを渡す
  />

2. Board内のrenderSquare(i)で渡ってきた配列にiが含まれているか(true/false)を渡す

renderSquare(i) {
  return <Square
            value={ this.props.squares[i] }
            onClick={ () => this.props.onClick(i) }
            isHighlight={ this.props.winLine.includes(i) } // 渡って来た配列にiが含まれているかどうか
            key={i}
          />;
}

3. Squareで渡って来た値がtrueだったら色をつける

<button
  className={
    props.isHighlight ? `square highlight-color` : `square` // trueなら highlight-color を反映する
    }
  onClick={props.onClick}
>

なんとかできました!!!

image.png

6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

これもまずさらっと実装方針を考えてみます

  • 勝ち負け判定にDrawを追加してstatusの部分で条件分岐を追加して表示させてあげる
  • 勝ち負け判定はcalculateWinner(squares)でやっていてsquaersには盤面の情報が入っている
  • squaersnullがなくなった時(盤面が全て埋まった時)がDrawということとする
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 { player: squares[a], line: [a, b, c] };
    }
  }
  // ここでDraw判定
  if (!squares.includes(null)) {
    return 'draw';
  }
  return null;
}

これで

  • どちらかが勝った時
    • { player: 'X' or 'O', line: [a, b, c] }
  • 引き分けの時
    • 'draw'
  • 決着が付いていない時
    • null

が返ってくるようになりました
条件分岐の順番を少し並び替えて下記のようにしたら…

let status;
let winLine = [];
if (winnerInfo === null) {
  status = `Next player: ${this.state.xIsNext ? 'X' : 'O'}`;
} else if (winnerInfo === 'draw') {
  status = `Draw`; 
} else {
  status = `Winner: ${winnerInfo.player}`; 
  winLine = winnerInfo.line;
}

image.png

引き分け時にDrawと表示できました!!
これで全部の追加課題が終了
お疲れさまでした=^o^=

おわりにちょっとした感想など

  • もっとリファクタ&改造できるところはあると思うが、とりあえずちゃんと表示されて動くものを作れるのが目標だったので、達成できてよかった
  • class間の(props)値の受け渡しの流れがまだふんわりとしかつかめていないのでので復習したい
  • でも動くもの作れたのはすごく楽しい
  • 情報を管理できるstateすごい…Reactだけで簡単なゲームが作れちゃうんだなぁ
  • 基本的なJS(ES6)がわかればできるのでReactチュートリアルすごい

まだまだ基礎が学べただけだと思うので、これからも勉強を続けて実務でコード書けるように頑張ります!
(そしていつかこのコードを改造&リファクタしたい)

hak_chami
Ruby/Railsエンジニアとしてなんとか頑張っています/最近UI/UX含めフロント部分に興味あり/JS(React)の勉強もはじめました(=^. .^=)
zeals-inc
チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』の開発・運営をやっています。
https://zeals.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした