React公式チュートリアルの改良問題に挑戦してみる

過去にprogateで基礎に触れてから長らく放置したままだったReactの勉強を本格的に始めることにしました。

というのも、遠くない未来で受ける案件において、Reactを使わないことは避けられそうにないからです。

その案件自体はVue.jsでも出来なくは無さそうですが、「せっかくなので一通り使えるようになりたい(むしろフロントエンドやるなら今時使えないとヤバい)」思っているのと、「Reactが分かるようになればWordPressのブロック開発も出来るようになる」という理由もあるからです。

とりあえず公式ドキュメントを読み始めて、チュートリアルの三目並べゲームを作りましたが、これが中々分かりやすくて面白く、回答が書かれていない改良問題にも早速挑戦してみましたので、その解答例を記載しておきます。

掲載元(React公式ドキュメント)

チュートリアル:React の導入 – React
ユーザインターフェース構築のための JavaScript ライブラリ

問題

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

3×3のマスのうち、左上をcol:1, row:1、右下をcol:3, row:3とします。

index.js

修正
class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null)
            }],
            stepNumber: 0,
            xIsNext: true,
            clickPosition: {    //追加
                col: null,
                row: null
            }
        };
    }
    /*略*/
}
修正
class Game extends React.Component {
    /*略*/
    handleClick(i) {
        /*略*/
        const position = getPosition(i);    // 追加
        /*略*/
        this.setState({
            history: history.concat([{
                squares: squares
            }]),
            stepNumber: history.length,
            xIsNext: !this.state.xIsNext,
            clickPosition: position    // 追加
        });
    }
    /*略*/
}
修正
class Game extends React.Component {
    /*略*/
    render() {
        /*略*/
        return (
        <div className="game">
            /*略*/
            <div>col:{this.state.clickPosition.col}, row:{this.state.clickPosition.row}</div>
            /*略*/
        </div>
        );
    }
}
追加
function getPosition(index) {
    let pos = [];
    for (let row = 1; row <= 3; row++) {
        for (let col = 1; col <= 3; col++) {
            pos.push({
                row: row,
                col: col
            });
        }
    }
    return pos[index];
}

新規定義したgetPosition関数は、下記のような配列を作って、クリックされた位置(0~8)と一致するインデックスの要素の値を返します。

{row:1, col:1},{row:1, col:2},{row:1, col:3},
{row:2, col:1},{row:2, col:2},{row:2, col:3},
{row:3, col:1},{row:3, col:2},{row:3, col:3},

「2. 着手履歴のリスト中で現在選択されているアイテムを太字にする。」

index.js

修正
class Game extends React.Component {
    /*略*/
    render() {
        /*略*/
        const moves = history.map((step, move) => { // step: value of array(history) , move: index of array(history)
            /*略*/
            const className = move === this.state.stepNumber ? 'active' : '' ;    // 追加
            return (    // className追加
                <li key={move} className={className}>
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                </li>
            );
        });
        /*略*/
    }
    /*略*/
}

move(0~8)がstateのstepNumberと一致するときはliタグにクラス名’active’を出力します。

index.css

追加
  li.active button {
    font-weight: bold;
  } 

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

index.js

修正
class Board extends React.Component {
    renderSquare(i) {
        return (
        <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
        key={i}/*追加*/
        />
        );
    }
    /*略*/
}
追加

“2 つのループ”を使う場合の処理内容に自信が無いですが、一応動きます。

class Board extends React.Component {
    /*略*/
    createBoardHtml() {    // 追加
        let square, div = [];
        let num = 0;
        for (let i = 0; i < 3; i++) {
            square = [];
            for (let j = 0; j < 3; j++) {
                num = 3 * i + j;
                square.push(this.renderSquare(num));
            }
            div.push(<div className="board-row" key={i}>{square}</div>);
        }
        return div;
    }
    /*略*/
}

renderSquare関数のSquareおよびcreateBoardHtml関数のdivにkey={i}を記述しないと下記警告を受けます。
Warning: Each child in a list should have a unique “key” prop.

リスト(配列)を出力する際は、それぞれにユニークなキーを付与する必要があります。

修正
class Board extends React.Component {
/*略*/
render() {
return (
<div>
{this.createBoardHtml()/*修正*/}
</div>
);
}
}

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

参考

ついつい押したくなる、CSS3を使ったラジオボタンのデザイン
フォームってあまり使うこともないのですが、CSS3を使ってデザインするとなかなか面白かったので、今回はラジオボタンを使ったサンプルを紹介します。...
Reactでinputを扱う - Qiita
Reactでinputを扱う(TypeScript版)という改訂版を書きました。input要素をReactで使うとReactがinputを自分でコントロールするので注意が必要。var React…

index.css

追加
  .toggle {
    display: flex;
  }
  .toggle input {
    display: none;
  }
  .toggle label{
    /*display: block;
    float: left;*/
    cursor: pointer;
    width: auto;
    margin: 0;
    padding: 0.25rem;
    background: #bdc3c7;
    color: #869198;
    font-size: 0.75rem;
    text-align: center;
    line-height: 1;
    transition: .2s;
  }
  .toggle label:first-of-type{
    border-radius: 3px 0 0 3px;
  }
  .toggle label:last-of-type{
    border-radius: 0 3px 3px 0;
  }
  .toggle input[type="radio"]:checked + .switch-on {
    background-color: #a1b91d;
    color: #fff;
  }
  .toggle input[type="radio"]:checked + .switch-off {
    background-color: #e67168;
    color: #fff;
  }

index.js

追加
class Toggle extends React.Component {
    render() {
        return (
            <div className="toggle">
                <span>History order:&nbsp;</span>
                <input type="radio" name="order" id="asc" value="asc" checked={this.props.order === 'asc'}
                    onChange={() => this.props.onChange()}/>
                <label htmlFor="asc" className="switch-on">ASC</label>
                <input type="radio" name="order" id="desc" value="desc" checked={this.props.order === 'desc'}
                    onChange={() => this.props.onChange()}/>
                <label htmlFor="desc" className="switch-off">DESC</label>
            </div>
        )
    }
}

トグルボタンを表示するコンポーネントです。inputとlabelについては、属性の値のみ定義したオブジェクトの配列を用意してarray.map()で出力した方がスマートかもしれません。

修正
class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {  
            /*略*/
            order: 'asc',        
        };
    }
}

昇順の設定状態を管理するstateを追加します。

追加
class Game extends React.Component {
    /*略*/
    handleChange() {
        const order = this.state.order === 'asc' ? 'desc' : 'asc';
        this.setState({
            order: order
        });
    }
    /*略*/
}

トグルボタンをクリックされた時の処理を定義します。(子コンポーネントToggleを通して呼び出される)

選択中のボタン(実態はradio)をクリックしても、onChangeイベントは発生しない(checkedされているradioは変わらない)ので、選択されている状態は取得せず単純に今のstateの状態(asc or desc)の逆をセットするだけで良いです。

この関数によってstateの変数が更新されると再レンダリングが発生するので、ここでヒストリーボタンの昇順並び替え処理を行う必要はありません。

修正
class Game extends React.Component {
    /*略*/
    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);

        let moves = history.map((step, move) => { // 修正:const -> let
            const desc = move ?
                'Go to move #' + move :
                'Go to game start';
            const className = move === this.state.stepNumber ? 'active' : '' ;
            return (
                <li key={move} className={className}>
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                </li>
            );
        });

        if (this.state.order === 'desc') {  // 追加
            moves = moves.reverse();
        }
        /*略*/
        return (
        <div className="game">
        /*略*/
            <Toggle
                order={this.state.order}
                onChange={() => this.handleChange()}
            />
        /*略*/
        </div>
        );
    }
}

変数movesは再代入を行う為にconstからletへ変更し、直後に昇順がdescなら逆順に並び替えます。

render()ではトグルボタンを表示するToggleコンポーネントを呼び出します。その際、昇順設定とonChange関数をpropsで渡します。

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

index.css

追加
  .win {
    background-color: cyan;
  }

index.js

修正
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]    // winner's line
        };
      }
    }
    return { // 修正
        'winner': null,
        'line': null
    };
}

勝者(O or X)に関する情報だけでなく、勝敗決定の要因となった勝者選択マスの座標情報lines[i](数値0~8を値として持つ要素数3の配列)も返すように、返り値の構造を変更します。

修正
class Game extends React.Component {
    /*略*/
    handleClick(i) {
        /*略*/
        if (calculateWinner(squares).winner || squares[i]) { // 修正
            return;
        }
        /*略*/
    }
    /*略*/
}

勝敗をチェックするcalculateWinner関数の返り値の構造を変更したので、それに合わせて修正します。

修正
class Game extends React.Component {
    /*略*/
    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const win = calculateWinner(current.squares);   // 修正
        const winner = win.winner;  // 追加
        const line = win.line;      // 追加
        /*略*/
        return ( // 修正
        <div className="game">
            /*略*/
            <Board
                line={line}
                squares={current.squares}
                onClick={(i) => this.handleClick(i)}
            />
            /*略*/
        </div>
        );
    }
    /*略*/
}

勝敗をチェックするcalculateWinner関数の返り値の構造を変更したので、それに合わせて修正します。

また、勝者選択マスの座標情報(null or配列)を”line”として子のBoardコンポーネントへ渡します。

修正
class Board extends React.Component {
    renderSquare(i) {
        let className = 'square';
        if (
            this.props.line !== null
            && this.props.line.indexOf(i) !== -1
        ) {
            className += ' win';
        }
        return (
        <Square
        className={className}
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
        key={i}
        />
        );
    }
    /*略*/
}

子のSquareコンポーネントに渡すcssのクラス名として”square”をデフォルトでセットし、親コンポーネントから受け取った勝者選択マスの座標情報の”line”(null or配列)がnullで無い時(勝利が確定している場合)に、描画しようとするマス番号i(0~8)が含まれていたら、cssのクラス名に” win”を追加します。

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

class名を直書きでは無く、親コンポーネントから受け取るように変更します。

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

index.js

修正
class Game extends React.Component {
    /*略*/
    render() {
        /*略*/
        let status ;
        if (winner) {
            status = 'Winner: ' + winner;
        } else {
            if (this.state.stepNumber == 9) // 追加
                status = 'Result:draw';
            else 
                status = 'Next player:' + (this.state.xIsNext ? 'X' : 'O');
        }
        /*略*/
    }
    /*略*/
}

利者判定の偽(else)の処理において、ステップ数(stepNumber)が9の場合は”Next player:”の代わりに”Result:draw”を表示します。

動作テスト用サンプル

See the Pen Answer example of React official tutorial by YUKI (@yuki84web) on CodePen.

タイトルとURLをコピーしました