手を動かしながら学ぶReact入門 3. 三目並べを関数コンポーネントで実装する

WebフレームワークであるReactについて、実際に手を動かしながら学んでいきたいと思います。

1.　[Create React Appでプロジェクトの雛形を作成](https://qiita.com/basio/items/998cbcb2be16e2f2a084)
2.　[Reactについて学ぶ](https://qiita.com/basio/items/80bdf2c1e59f84777f90)
3.　三目並べを関数コンポーネントで実装する　←　ここ

# 作成するアプリケーション
[React公式のチュートリアルで実装する三目並べ](https://ja.reactjs.org/tutorial/tutorial.html)を実装します。
チュートリアルではクラスコンポーネントで実装されていますが、本稿では関数コンポーネントだけを用いて実装します。
通常の三目並べに加え、特定の手数まで戻るタイムトラベル機能がついています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556091/e36b1dcd-2f37-34a3-e997-4a139dc5db1a.png)


## コンポーネントの構成
下記のようなコンポーネントの構成で実装していきます。
GameコンポーネントはBoardコンポーネントを子の要素として持ち、Boardコンポーネントは9個のSquareコンポーネントを子として持ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556091/4bb376df-5356-adee-e0c3-2fe1122ba948.png)

# 単純な三目並べの実装（Boardまで）
まずはタイムトラベル機能を持たない、単純な三目並べを実装していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556091/4da05e13-3d17-67f9-4ed8-996699cfc191.png)
## Square コンポーネント
9個ある正方形の升目をボタンとして実装します。
`props`をBoardから受け取り、`props.value`を表示、クリック時には`props.onClick`をコールするようにします。

```jsx:App.jx
import './App.css'; // スタイル適用のためにcssをインポート

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

スタイルを適用させるため、App.cssをApp.jsと同じフォルダに作成し、下記のように記載します。

```css:App.css
.square{
  height: 50px;
  width: 50px;
  vertical-align: top;
}
```

## Board コンポーネント
### [1/3] Squareを並べてみる
まずはSquareコンポーネントを3×3に並べてプロパティを設定してみましょう。
`<Square/>`を一つ一つ`<div>`の中に書いてもいいのですが、見通しと保守性を考えて、`value`を引数に持つ`renderSquare`でSquareコンポーネントを作成するようにします。
この時に`value`と`onClick`をpropsとして渡しているのがわかりますね。

```jsx:App.js
function Board(){
  
  function handleClick(value){
    alert(value);
  };

  function renderSquare(value){
    return(
      <Square value={value} onClick={()=>handleClick(value)}/>
    );
  };

  return(
    <div>
      <div>
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div>
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div>
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  )
}

export default Board
```
下記のように3×3のボタンが表示され、ボタンをクリックするとボタンと同じ番号がalertされるのが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556091/f9cf7fa4-c3b9-6951-4ee4-5f208ddb390f.png)

### [2/3] stateを実装する
三目並べとしたいので、Squareの中に書かれている内容は数字ではなくOかXかを表示させる必要があります。
そのために、現在の盤面を下記のように配列で表現し、これをstateとして保存するようにします。

```jsx
var squares = [ null, null, 'X'
                'X' , 'O' , null
                null, null, null]
```
また、手番を交代とするために、`xIsNext`というstateも持つようにします。

まずはstateを利用するために、`useState`をインポートします。

```diff_jsx:App.js
+ import React, {useState} from 'react';
import './App.css';
```

`handleClick`、`renderSquare`を下記のように更新することで、ボタンをクリックすると交代でXとOが入力されるようになります。
`handleClick`内で`squares`をそのまま使わずにコピーを取っている理由については、[Reactのチュートリアル](https://ja.reactjs.org/tutorial/tutorial.html#why-immutability-is-important)を参照してください。

```jsx:App.js
import React, {useState} from 'react' // useState利用のためにimportする

// function Square()...

function Board(){
  const [squares, setSquares] = useState(Array(9).fill(null)); // 盤面
  const [xIsNext, setXIsNext] = useState(true);                // 手番

  function handleClick(value){
    const newSquares = squares.slice();     // squaresのコピーを取得
    newSquares[value] = xIsNext? 'X' : 'O'; // 手番に応じてXかOを設定
    setSquares(newSquares);                 // squaresを更新
    setXIsNext(!xIsNext);                   // 手番を更新
  };

  function renderSquare(value){
    return(
      <Square value={squares[value]} onClick={()=>handleClick(value)}/> // valueとしてsquares[value]を渡すように変更
    );
  };
  //return(...
}
```

### [3/3] 勝敗の判定を実装する
盤面の状態に応じて、手番の表示と勝敗の判定を行う部分を実装していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556091/ed569265-d97a-28c3-8a50-50fd46f73da6.png)

まずは手番を表示できるようにします。
メッセージを表す`status`stateを定義し、それを表示する`<div>`タグを記述します。

```jsx:App.js
function Board(props){
  
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);
  const [status, setStatus] = useState("");


//  function renderSquare(value){
//  ...
//  };

  return(
    <div>
      <div>{status}</div> {/*追加*/}
      <div>
      {/*renderSquare(...)*/}
      </div>
    </div>
  )
}
```

このstatusを手番ごとに更新するのですが、ここでは`useEffect`というHookを利用します。
`useEffect`はViewが更新される度にコールされ、ここで`status`メッセージを更新することにします。
`useEffect`を利用するために、reactから`useEffect`をインポートしておきます。


```diff_jsx:App.js
- import React, {useState} from 'react';
+ import React, {useState, useEffect} from 'react';
import './App.css';
```

勝敗を判定する関数は公式のものをコピーしてきます。

```jsx:App.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 squares[a];
    }
  }
  return null;
}
```

`useEffect`を実装していきます。`calculateWinner`が`null`でない場合は`status`に勝者を表示、そうでない場合は手番を表示します。

```jsx:App.js
function Board(props){
  // ...

  // function renderSquare(value){
  // ...
  // };

  useEffect(()=>{
    const winner = calculateWinner(squares);
    if(winner){
      setStatus("Winner : " + winner);
    }
    else{
      setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
    }
  })

  // return(...
}
```

このままだと、ゲーム終了後にも盤面をクリックできてしまう＆同じボタンが何度も押せてしまうので、下記のように`handleClick`を修正します。

```jsx:App.js
  function handleClick(value){
    if (calculateWinner(squares) || squares[value]) {
      return;
    }
    // ...

  };
```

### App.js全体
App.jsの全体は下記のようになります。

```jsx:App.js
import React, {useState, useEffect} from 'react';
import './App.css';

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

function Board(props){
  
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);
  const [status, setStatus] = useState("");

  function handleClick(value){
    if (calculateWinner(squares) || squares[value]) {
      return;
    }
    const newSquares = squares.slice();
    newSquares[value] = xIsNext? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

  function renderSquare(value){
    return(
      <Square value={squares[value]} onClick={()=>handleClick(value)}/>
    );
  };

  useEffect(()=>{
    const winner = calculateWinner(squares);
    if(winner){
      setStatus("Winner : " + winner);
    }
    else{
      setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
    }
  })

  return(
    <div>
      <div>{status}</div>
      <div>
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div>
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div>
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </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 squares[a];
    }
  }
  return null;
}

export default Board;

```
# タイムトラベル機能の実装（Gameの実装）
ここで完成形となるGameコンポーネントを実装します。
今まで作成したBoardコンポーネントを子要素に持たせ、今までの三目並べの機能に加えて、任意の手番まで戻ることができるタイムトラベル機能を実装します。

タイムトラベル機能の実現のため、過去の手番すべてを記憶させた、`history`配列を用意します。

```jsx
var history = [
    {
        squares: [ null, null, null
                   null, null, null
                   null, null, null]
    },
    {
        squares: [ null, null, null
                   null, 'X' , null
                   null, null, null]
    },
    {
        squares: [ null, null, 'O' 
                   null, 'X' , null
                   null, null, null]
    },
    // ...
```

### [1/3] stateのリフトアップ
上記の`history`というstateはゲーム全体に関わる要素なので、Gameコンポーネントのstateとして保持することにします。
こうすることで、Boardコンポーネントは`squares`というstateを保つ必要がなくなり、親要素であるGameコンポーネントから逐一`squares`を受け取り、それを描画するだけでよくなります。
これに合わせて、stateを変更する`handleClick`や、他のstate（`xIsNext`、`status`）もGameコンポーネントにリフトアップさせます。

```jsx:App.js
function Game(){
  const [history, setHistory] = useState([{squares: Array(9).fill(null)}]);
  const [xIsNext, setXIsNext] = useState(true);
  const [status, setStatus] = useState("");

  function handleClick(value){
    const current = history[history.length - 1];
    
    if (calculateWinner(current.squares) || current.squares[value]) {
      return;
    }
    const newSquares = current.squares.slice();
    newSquares[value] = xIsNext? 'X' : 'O';
    setXIsNext(!xIsNext);
    setHistory(history.concat([{squares: newSquares}]));
  };

  useEffect(()=>{
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);
    if(winner){
      setStatus("Winner : " + winner);
    }
    else{
      setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
    }

  });

  return(
    <div>
      <div>
        <Board squares={history[history.length - 1].squares} onClick={(i)=>handleClick(i)}/>
      </div>
      <div>
        <div>{ status }</div>
      </div>
  </div>
  );
}
```

Boardコンポーネントはstateを扱う必要がなくなり、親要素からpropsとして受け取った値だけを表示するだけでよくなります。

```jsx:App.js
function Board(props){

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

  return(
    <div>
      <div>
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div>
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div>
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  )
}
```

### [2/3] 過去の手の表示
過去の手に戻る、下記部分の表示を実装していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556091/e7de0d38-23c3-ad45-5596-81038366019d.png)

`history`に対して`map`を行い、その結果を`moves`として表示させるようにします。
ループでリストの要素を追加しているため、key要素を割り当てる必要があります。詳細は[こちら](https://ja.reactjs.org/docs/lists-and-keys.html#keys)。
ボタンをクリックした際にコールされる`jumpTo`は後ほど実装します。

```jsx:App.js
function Game(){
  // ...

  const moves = history.map((step, move) => {
    const desc = move ?
      'Go to move #' + move :
      'Go to game start';
    return (
      <li key={move}> {/*リストにはKeyを割り当てる必要がある*/}
        <button onClick={() => jumpTo(move)}>{desc}</button> {/*jumpToは後ほど実装*/}
      </li>
    );
  });
  
  // ...

  return(
    <div className="game">
    <div className="game-board">
      <Board squares={history[stepNumber].squares} onClick={(i)=>handleClick(i)}/>
    </div>
    <div className="game-info">
      <div>{ status }</div>
      <ol>{moves}</ol> {/*追加*/}
    </div>
  </div>
  );
}

```

### [3/3] タイムトラベルの実装
タイムトラベル機能の実装にあたって、現在の手が何番目かを表す`stepNumber`というstateを定義します。

```diff_jsx:App.js
function Game(){
  const [history, setHistory] = useState([{squares: Array(9).fill(null)}]);
  const [xIsNext, setXIsNext] = useState(true);
  const [status, setStatus] = useState("");
+ const [stepNumber, setStepNumber] = useState(0);

  // ...
}
```

この`stepNumber`は手番が進むたびに更新される必要があるため、`handleClick`の中で更新されるようにします。
また、それに合わせて現在の盤面を表す`current`を`stepNumber`から取得するように変更します。

```diff_jsx:App.js
function Game(){
  // ...

  function handleClick(value){
+   const hist = history.slice(0, stepNumber + 1);
+   const current = hist[hist.length - 1];
-   const current = history[history.length - 1];
    
    if (calculateWinner(current.squares) || current.squares[value]) {
      return;
    }
    const newSquares = current.squares.slice();
    newSquares[value] = xIsNext? 'X' : 'O';
    setXIsNext(!xIsNext);
+   setStepNumber(hist.length);
    setHistory(hist.concat([{squares: newSquares}]));
  };

  useEffect(()=>{
+   const current = history[stepNumber];
-   const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);
    if(winner){
      setStatus("Winner : " + winner);
    }
    else{
      setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
    }

  });

  // ...
}
```

最後に、未定義だった`jumpTo`メソッドを下記のように定義して完成となります。

```jsx:App.js
function Game(){
  // ...

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }
  // ...
}
```

お疲れさまでした！これで三目並べの関数コンポーネントでの実装は完了です！
最後に、App.js全体のコードを貼り付けておきます。

```jsx:App.js
import React, {useState, useEffect} from 'react';
import './App.css';

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

function Board(props){

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

  return(
    <div>
      <div>
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div>
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div>
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  )
}

function Game(){
  const [history, setHistory] = useState([{squares: Array(9).fill(null)}]);
  const [xIsNext, setXIsNext] = useState(true);
  const [status, setStatus] = useState("");
  const [stepNumber, setStepNumber] = useState(0);

  function jumpTo(step){
    setStepNumber(step);
    setXIsNext((step%2) === 0);
  }

  const moves = history.map((step, move) => {
    const desc = move ?
      'Go to move #' + move :
      'Go to game start';
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });
  
  function handleClick(value){
    const hist = history.slice(0, stepNumber + 1);
    const current = hist[hist.length - 1];
    
    if (calculateWinner(current.squares) || current.squares[value]) {
      return;
    }
    const newSquares = current.squares.slice();
    newSquares[value] = xIsNext? 'X' : 'O';
    setXIsNext(!xIsNext);
    setStepNumber(hist.length);
    setHistory(hist.concat([{squares: newSquares}]));
  };

  useEffect(()=>{
    const current = history[stepNumber];
    const winner = calculateWinner(current.squares);
    if(winner){
      setStatus("Winner : " + winner);
    }
    else{
      setStatus('Next player: ' + (xIsNext ? 'X' : 'O'));
    }

  });

  return(
    <div className="game">
    <div className="game-board">
      <Board squares={history[stepNumber].squares} onClick={(i)=>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 squares[a];
    }
  }
  return null;
}

export default Game;
```