ボクココ

個人開発に関するテックブログ

React チュートリアルで学んだことと解答例

ども、@kimihom です。

f:id:cevid_cpp:20190812163412j:plain

いよいよ React を学ぶ機会が出てきたので、その経緯と Hello React のログを残しておく。

利用のきっかけ

まず、自前の Web アプリケーションを React で書くってのは今でも too much だと思っている。この点に関しては以前の記事に記している。

www.bokukoko.info

今までは React よりも他に自分にとって関係の深い技術を積極的に学んだきたわけだけども、いよいよ React が必要になるケースが出てきた。それが、 Twilio Flex の出現である。Twilio Flex は、カスタマイズ可能なコンタクトセンタープラットフォームで、今 Twilio が一番力を入れて開発しているプロダクトである。

twilio.kddi-web.com

「カスタマイズ可能」ってことなんだけど、この Twilio Flex をカスタマイズするには、React の技術習得が必要不可欠になる。なるほど、Twilio Flex のような複雑なシステムに機能を効率良く追加・編集するのに、React を選択したってのは理解できる。てことで、Twilio Flex を拡張するために必要な技術ってことで React の勉強を始めたわけである。

React チュートリアルでの学習

React のページに行くと、しっかりとチュートリアルが用意されている。これがとてもわかりやすくて良かった。

ja.reactjs.org

一通りこのページを読み終えると、以下の記述があった。

時間がある場合や、今回身につけた新しいスキルを練習してみたい場合に、あなたが挑戦できる改良のアイデアを以下にリストアップしています。後ろの方ほど難易度が上がります:

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

これは自分の理解をチェックするためにもやるしかないなってことで、5と6だけやった。本記事の最後にコードを掲載している。

React を初めて学んでみて、特徴的だと感じたことを以下メモとして記す。

リフトアップの概念

チュートリアルを読むと、最初は Square(9つ四角の一つ一つ)でそれぞれデータを管理するプログラムだが、各 Square の状態を管理するために Square の状態管理を Board の状態管理へ移し、そのあと履歴を管理するために Game の状態管理 へと移っていっている。 State のリフトアップ というらしく、これが React において重要な概念であると感じた。

複数の子要素からデータを集めたい、または 2 つの子コンポーネントに互いにやりとりさせたいと思った場合は、代わりに親コンポーネント内で共有の state を宣言する必要があります。親コンポーネントは props を使うことで子に情報を返すことができます。こうすることで、子コンポーネントが兄弟同士、あるいは親との間で常に同期されるようになります。

イミュータブル の意識

今までの JavaScript コーディングであまり気にしてこなかったんだけど、イミュータブル(新しいデータのコピーで古いデータを置き換える) ってのが大事になるようだ。以下の記述がある。

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

なるほど、今回のチュートリアルの履歴の管理は、まさにイミュータブルに管理したからこそ実現できた機能であることがわかる。打ったマスの状態を配列で管理することで、戻ったり進んだりすることが可能になっている。 データを直接編集するメソッドではなく、戻り値で変わった値が返ってくるメソッドを使う意識を持とう。

リストの key 指定

リストを React で表示するときに、key の指定が大事になるらしい。パフォーマンスの視点からとのことなので、忘れないようにしよう。

        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>

改良のアイデア の補足

本記事の最後に掲載したコードには、 5 と 6 の改良のアイディアが実装されているので、ちょっとだけ解説する。

  1. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
  2. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

まずハイライトってことで、最終的に Square の CSS の背景色を yellow にすることにした。その時点で、Square に勝ちの Square か、通常の Square かを分ける必要がある。てことで、props に isWin を追加した。

  renderSquare(i) {
    return <Square
      value={this.props.squares[i]}
      isWin={this.props.winner && this.props.winner.line.includes(i)}
      onClick={() => this.props.onClick(i)}
    />;
  }

winner には勝った プレイヤー X or O がデフォルトで入っていたんだけど、これだと どの Square が勝ちのマスだかわからないので、winner をハッシュ形式に変えた。

before: winner = "X"
after: winner = {winner: "X", line: [0,1,2]}

Winner: ${winner.winner} ってコードになっちゃって微妙だけど、ひとまず これで勝った時の line だけスタイルを変えることができるようになった。より詳細はコードを読んでいただければと思う。

引き分け表示が一番最後の問題で一番難しいってことなんだけど、そうでもなかった。単に winner がまだいなくて かつ history が最後だったら表示ってだけで実装できた。

    if (winner) {
      status = `Winner: ${winner.winner}`;
    } else if (history.length === current.squares.length + 1) {
      status = "Draw";
    }

終わりに

Hello React!

ひとまず入門ってことで概要を理解することができた。こうやってブログに書くことで、自分の理解をより強固なものにできる。もし自分の変えたコードを「こうしたらもっとよくできる」みたいなアドバイスがあれば、ぜひ教えていただけたら幸いだ。

引き続き React 学習を進めていくので、新しいことを理解したらブログに記そう。

チュートリアル の ソースコード

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

// 関数コンポーネント
// render メソッドだけを有して自分の state を持たない。
// Sqare は Board に制御されたコンポーネント
// この場合よりシンプルに実装できる。引数に props が出てくる
function Square(props) {
  return (
    <button
      className="square"
      onClick={props.onClick}
      style={{backgroundColor: props.isWin ? 'yellow' : 'white'}}
    >
      {props.value}
    </button>
  );
}

// props, state の2つは予約語
// props は react dom に記された attribute. 読み込み専用!
// state は constructor で定義されたインスタンス変数. 
// state の更新
//  - setState を呼び出す. 
//  - 値はマージされる
//
// setState で現在の state, props 値を使う場合は関数化する
// NG
// this.setState({
//  counter: this.state.counter + this.props.increment,
// });
// OK
// this.setState((state, props) => ({
//  counter: state.counter + props.increment
// }));

class Board extends React.Component {
  renderSquare(i) {
    return <Square 
      value={this.props.squares[i]}
      isWin={this.props.winner && this.props.winner.line.includes(i)}
      onClick={(e) => {this.handleClick(i, e)}}
      //onClick={this.handleClick.bind(this, i)}
    />;
  }

  handleClick(i, e) {
    e.preventDefault(); // NG: return false;
    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) {
    // state.squares のコピーを `slice` で作成
    // イミュータブルにすることで 履歴の操作、変更検出の利点
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    // 決着がついている or マスが埋まっている
    if (calculateWinner(squares) || squares[i]) return;
    squares[i] = this.state.xIsNext ? 'O' : 'X';
    this.setState({
      // push ではなく concat. イミュータブル
      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);
    let status = `Next player ${this.state.xIsNext ? 'X' : 'O'}`;
    if (winner) {
      status = `Winner: ${winner.winner}`;
    } else if (history.length === current.squares.length + 1) {
      status = "Draw";
    }

    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}
            winner={winner}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

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 {winner: squares[a], line: lines[i]};
    }
  }
  return null;
}

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