React.jsのチュートリアルをやってみました


今までフロントエンドは Vue.js を使っていたのですが、React.js を使ったことがなかったため、チュートリアルの「三目並べゲーム」を実装してみました。
チュートリアルには書かれていなかったことやわかりにくかった部分を補足として追記してあります。


*参考



*環境

  • MacOS
  • Node 10.7.0
  • nodeblew v11.14.0
  • npm 6.3.0


*インストール

Node.js の v4.x 以上が必要です。
下記コマンドを実行して Node.js をインストールし、ローカルで使うバージョンを設定します。
$ nodebrew install-binary latest

# バージョン確認
$ nodebrew ls
v9.4.0
v11.14.0
current: v11.14.0

# 使うバージョンを設定
$ nodebrew use {vX.X.X}

ターミナルを再起動してから下記コマンドを実行し、バージョンが変更されていることを確認します。
$ node -v
v10.7.0


*Reactプロジェクト作成

下記コマンドを実行してアプリを作成します。
$ npx create-react-app react-app

処理完了後、作成されたディレクトリに移動してアプリを起動すると、Reactのロゴが表示された画面が表示されます。
$ cd react-app/
$ npm start












*初期表示するための準備

新しく作り直すため、srcディレクトリ 配下のファイルを削除します。
$ cd src/
$ rm -f *

空になったsrcディレクトリ配下にindex.cssindex.jsを新規作成します。
<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;
  }
  

<index.js>
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('root')
);

下記コマンドを実行してアプリを起動すると、三目並べゲームが表示されます。
$ npm start













*値の受渡し処理を追加

propsを使ってパラメータの受け渡しをして、render()で表示する階層構造を返します。
JSXを使うことで、下記のようなHTMLタグをコードに埋め込むことができるようになっています。

index.jsを下記に修正し、BoardからSquareコンポーネントへvalueの値を渡すようにします。
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} />;
    }



*状態の記憶を追加

stateを使って状態を記憶させることができます。
stateを使う際は、まずコンストラクタを追加して初期化します。
(コンポーネントのコンストラクタを追加する場合はsuper(props)を呼ぶ必要があります。)

setState()を使うことで再レンダリングさせることができます。
下記コードに修正すると、マス目をクリックするとthis.state.valueが再レンダリングされ、更新された「X」が表示されるようになります。
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>
        ); 
    }
}


*状態を一括で管理

Boardコンポーネントにコンストラクタを追加し、stateを持たせます。
こうすることでマス目(Square)をクリックするとonClickが呼び出され、Boardコンポーネントで状態を保持することができます。
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)}
            />
        );
    }
}

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

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


*手番の変更処理を追加

Squareコンポーネントを関数化します。
こうすることで完結に書くことができます。

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

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

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

次にxIsNextの変数を追加し、次の手番がXOかを管理できるようにします。
class Board extends React.Component {
    constructor(props) {
        super(props);
        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,
        });
    }

...

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


*勝者の判定処理を追加

縦、横、斜めのいずれか3マスを取ったほうが勝ちなので、そのマス目を取得しているかを判定する関数を追加します。
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]
        }
    }
}

Boardコンポーネントで勝者を表示できるようにします。
すでに勝敗が決まっている場合、ドローになった場合は早期リターンさせます。
class Board extends React.Component {
...
    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,
        });
    }

    render() {
        const winner = calculateWinner(this.state.squares);
        let status;
        if (winner) {
            status = 'Winner: ' + winner;
        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
...

}


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

何手目にどこのマス目をクリックしたか履歴として記録しておき、過去の手番に戻れる機能を追加します。
Gameコンポーネントがstateにアクセスできるようにするため、BoardコンポーネントのstateGameに移植します。(リフトアップといいます)

また、下記についても修正します。
  • render()history(最新の履歴) を使うよう修正
  • Boardコンポーネントに値を渡すよう修正
  • BoardコンポーネントのhandleClickGameに移植
  • コンストラクタに何手目かを管理するstepNumberを追加
  • stepNumberを新しい手が追加されるたびに更新するよう修正
class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
            }],
            // 何手目かを管理
            stepNumber: 0,
            xIsNext: true,
        };
    }

 // Board から移植
    handleClick(i) {
     // 巻き戻ったときに将来あった履歴を削除
  const history =  this.state.history.slice(0, this.state.stepNumber +  1);
  // 現在の手を管理
        const current = history[this.state.stepNumber];
        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,
            // 偶数の次の手が'X'になるよう更新
            xIsNext: (step % 2) === 0,
        });
    }
   
    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 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 に値を渡すよう修正
                    <Board
                        squares={current.squares}
                        onClick={(i) => this.handleClick(i)}
                    />
                </div>
                <div className="game-info">
                 // 次の手番、勝敗の状態をstateを使って表示
                    <div>{status}</div>
                    <ol>{moves}</ol>
                </div>
            </div>
        );
    }
}

次にGameコンポーネントから値を受け取るようにします。
Boardのコンストラクタを削除し、renderSquare()で Square から受け取る値をpropsに書き換えます。
class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square 
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
            />
        );
    }

 // Game コンポーネントに移植した部分を削除
    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>
        );
    }












*所感

クラス設計をしっかり行う必要があるので、Vue.js と比べると React.js のほうが難易度は高いと感じましたが、大規模なアプリケーションを作成する場合は React.js のほうが適しているかと感じました。
また、私は業務で Java を使っていたことがあるので、クラスの責務や関係性を意識するといった面では、React.js の考え方と似ている部分がありました。
設計する力や根本的な Javascript の技術力といった面では React.js を使ったほうが勉強になりそうなので、今後も少しずつ勉強していこうと思います。

Previous
Next Post »

人気の投稿