LoginSignup
3
1

More than 3 years have passed since last update.

Reactのチュートリアル追加課題解いてみた

Last updated at Posted at 2020-12-16

はじめに

Reactチュートリアルの追加課題の解答と、私が解いていく中で詰まったところを記述する。ReactやJSに関する知識が拙い中解いたので、より良い解き方などがあると考えられるが、一例として参考にしていただきたい。(より良い解き方など指摘いただければ追記致します。)
コードはチュートリアル内の最終結果を改変した。

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

考え方

着手の位置を履歴として残すために、historyに新たな要素として追加する。その後movesに着手の位置も表示するように変更すればよい。

解答例

まず、Gameクラスのコンストラクタにあるhistoryと盤面が動いた際の処理が書かれたメソッドhandleClick内のhistoryputStateをkeyとした着手の位置を保存するための要素を追加する(history自体ではなくhistory内にある辞書やhistoryに追加する辞書に対しての意)。
コンストラクタでは以下のように変更する(ここで定義しなくても動く)。

history: [
    {
        squares: Array(9).fill(null),
        putState: "(0, 0)"
    }
]

だが、handleClickメソッドでは着手の位置を与える必要がある。幸運なことに(当然だが)このメソッドは選択したマスの番号を引数として与えられているので、以下のように変更するだけでよい。

history: history.concat([
    {
        squares: squares,
        putState: ` (${i/3|0}, ${i%3})`
    }
])

JavaScriptのバージョンによっては文字列の埋め込みがこのようにできないので以下のようにする。

history: history.concat([
    {
        squares: squares,
        putState: " (" + (i/3|0).toString() + ", " + (i%3).toString() + ")"
    }
])

このようにして着手の位置を履歴として残せるようになったので最後にmovesを変更し、着手の位置を表示させる。

const moves = history.map((step, move) => {
    const desc = move ?
        'Go to move #' + move + step["putState"]:
        'Go to game start';
    return (
        <li key={move}>
            <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
    );
});

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

考え方

選択されている盤面はGameクラスのメンバ変数StepNumberに格納されている。また、着手履歴の表示に関する部分はmovesに格納されている。このmovesは着手履歴を保存しているhistoryを用いてrenderされる度に生成される。さらに表示したい盤面を着手履歴のリストから選択するたびStepNumberが更新され再度renderされる。つまり、選択のたびrenderされるのでmovesが生成されるときにStepNumberと一致する番目にあるアイテムをボールドするようにすれば良いと考えられる。

解答例

変更点はmovesの定義部分のみ

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)}>
                {
                    move === this.state.stepNumber
                    ? <b>{desc}</b>
                    : desc
                }
            </button>
        </li>
    );
});

最初falseの時のdesc{desc}と書いていてerror({}が二重になるため)を出していたのでこの部分は注意したい。

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

考え方

単純にリストを作って二重ループ回してリストにJSXを適宜格納し、ハードコーディング部分をリストに置き換える。

解答例

考え方通りコードを書く。

render() {
    const squareBoard = [];
    const row = 3;
    const col = 3;
    for(let i=0; i<row; i++){
        let rowBoard = [];
        for(let j=0;j<col; j++){
            rowBoard.push(this.renderSquare(j+3*i));
        }
        squareBoard.push(
            <div className="board-row">
                {rowBoard}
            </div>
        )
    }
    return (
        <div>
            {squareBoard}
        </div>
    );
}

これで上手くいくと考えていたが警告が出る。

Each child in a list should have a unique "key" prop.
```これはkeyに関する警告でチュートリアル中にも触れられていたものである。keyを設定することで[VirtualDOM](https://stackoverflow.com/questions/21965738/what-is-virtual-dom)のdiffから実際のDOMに反映させるときの変更を最小限にすることができる。変更を施すと以下のようになる。

```Javascript
class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
                key={"square-"+i.toString()}
            />
        );
    }

    render() {
        const squareBoard = [];
        const row = 3;
        const col = 3;
        for(let i=0; i<row; i++){
            let rowBoard = [];
            for(let j=0;j<col; j++){
                rowBoard.push(this.renderSquare(j+3*i));
            }
            squareBoard.push(
                <div className="board-row" key={"row-"+i.toString()}>
                    {rowBoard}
                </div>
            )
        }
        return (
            <div>
                {squareBoard}
            </div>
        );
    }
}

ここでは"row-"+i.toString()としているがiだけでも問題ない(Squareも同様)。

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

私はトグルボタンそのものを実装することは本質ではなく、コードが複雑になると考えたので単純にボタンで実装した(トグルボタンにしたい方はトグルボタンのコンポーネントを作成してボタンのタグをトグルボタンのタグに変更することでできる)。

考え方

着手履歴は、Gameクラスのメンバ変数historyに格納されており、movesによって実装されている。moveshistoryから履歴を一手ずつ取ってきてJSXにしたものをリストにしている。historyは常に昇順であるためこれから作られたmovesもまた常に昇順となっている。つまり降順で表示したいときはmovesを逆順にすると良いと考えられる。

解答例

昇順、降順ボタンを任意の位置(ここでは着手履歴の上)に設定し、それを押すことで新たに追加するGameのメンバ変数ascendingの値を変える。ascendingは昇順でtrue、降順でfalseとなるようにする。ascendingの値によってrenderの直前にmovesを逆順にするかどうかを決める。

constructor(props) {
    super(props);
    this.state = {
        history: [
            {
                squares: Array(9).fill(null)
            }
        ],
        stepNumber: 0,
        xIsNext: true,
        ascending: true,
    };
}
render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    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");
    }

    if(!this.state.ascending) moves.reverse();

    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>
                <button onClick={()=>this.setState({ascending: true})}>昇順</button>
                <button onClick={()=>this.setState({ascending: false})}>降順</button>
                <ol>{moves}</ol>
            </div>
        </div>
    );
}

一つ目はGameクラスのコンストラクタ部分で、二つ目はGameクラスのrender部分である。トグルボタンでは押したときにascendingをtrueやfalseを切り替えるように実装してやれば良い。

<ToggleButton onClick={()=>this.setState({ascending: !ascending})}/>

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

考え方

勝敗が決定したときにハイライトさせるので勝敗を確認するcalculateWinner関数と勝敗によって値が変わるGameクラス内のstatusを定義している部分をうまく変更すれば良いと考えられる。まず勝敗につながった3マスをどのように抜き出すかを考える。calculateWinner関数では勝利につながる可能性のある3マスの組を全列挙しそのうちの一つでも3マス全てが同じ記号であればその記号を返し、1つもなければnullを返している。つまり、calculateWinner関数では勝敗を決まった時勝利につながった3マスを特定することができ、返り値にその3マスも返すようにすることが可能であると考えられる。次に抜き出した情報をもとにハイライトを行う方法を考える。勝敗が決まった時のみこの処理を行うのでハイライトを行う機能はstatusを定義する場面で行う。勝敗が決まった時の処理でcurrent.squaresの先ほど抜き出した3マスをハイライトする機能を追加すると良いと考えられる。

解答例

まず、clulculateWinner関数の返り値を既存のものと勝利につながった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], lines[i]];
        }
    }
    return [null, []];
}

返り値を変えたことによってhandleClickメソッドに少々の変更を行う。

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {//ここだよ
        return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
        history: history.concat([
            {
                squares: squares
            }
        ]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext
    });
}

最後にstatus内の処理に一手間加える。

let status;
if (winner[0]) {
    status = "Winner: " + winner[0];
    for(let i in winner[1]) current.squares[winner[1][i]] = <font color="#F00">{current.squares[winner[1][i]]}</font>;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

ハイライトは3つのマスを赤色にすることにした(これは何でもいい)。
しかし、この方法だと勝利につながったマスが5つの時であっても3つしかハイライトされない。このようなケースは通常人間同士が真剣に行ったときに出ることはないが、気持ち悪いので5つともハイライトされるようにする。変更したのはcalculateWinner関数のみ。

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]
    ];
    let winner = null;
    let winLines = [];
    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]) {
            winLines = winLines.concat(lines[i]);
            winner = squares[a];
        }
    }
    return [winner, winLines];
}

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

考え方

引き分けのメッセージはstatusに新たな分岐を作ることによって、引き分けの判別はcalculateWinner関数で引き換えの処理を加えればよいと考えられる。また、引き分けは勝敗が決まってない時のsquaresの中身にnullがあるかないかで判別することができる。

###解答例
まず、statuscalculateWinner関数の返り値に依存しているので、calculateWinner関数を変更する。この関数はhandleClickメソッドにも使われており、それに影響が出ないような形で変更したい。つまり、bool値がtrueとなるような値になるようにする。

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];
        }
    }

    if (squares.indexOf(null) === -1) return "draw";

    return null;
}

最後にstatusの定義部分を変更する。引き分けになった時の表示はDrawにした。

let status;
if (winner === "draw") {
    status = "Draw";
} else if (winner) {
    status = "Winner: " + winner;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

おわりに(全体コード)

追加課題を全て終えた後はこのようなコードになった。より良い書き方や書き方の問題などがあれば追記するので教えていただきたい。

3
1
0

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
3
1