Vue.js版React.js公式チュートリアル

# Vue.js版React.js公式チュートリアル

## 初めに
これまでReactを勉強してきて、Vueは触れてなかったのですが、後輩にフロントエンド教える機会があったため勉強し始めました。その際、[Reactの公式チュートリアル](https://ja.reactjs.org/tutorial/tutorial.html)をVueで書き直してみれば、色々学べるのではないかと思い試してみました。

##スターターコード
以下のコードは全てCodeSandBoxを用いて記述しました。以下に勉強用のスターターコードをおいておきます。
[CodeSandBox](https://codesandbox.io/s/vue-tutorial-starter-code-4dpiq?file=/src/Square.vue)

## ゲームを完成させる
以下のコードは[Vue公式ドキュメント｢はじめに｣](https://v3.ja.vuejs.org/guide/introduction.html#vue-js-%E3%81%A8%E3%81%AF)を読めばほとんど理解できると思います。事前に読むことを推奨します。また基本的な流れはReact公式チュートリアルと同じです。しかし、Reactとは仕様が異なり公式チュートリアルから変更した点も多いため、以下ではその部分を中心に解説します。 
※Reactを読める方であれば公式チュートリアルのコードと比較したほうが早いかもしれません。

### データをProps経由で渡す
Boardコンポーネントで渡されたpropsをもとにボタンの文字を変更するようにしています。

```vue:Square.vue
<template>
  <button class="square">{{ value }}</button>
</template>

<script>
export default {
  props: ["value"],
};
</script>
```

### インタラクティブなコンポーネントを作る
以下ではpropで管理していた`value`をdataで管理するように変更しています。また、そのヘルパー関数としてmethodsに`handleClick`関数を定義しています。 
v-onディレクティブは、`v-on:イベント`の代わりに`@イベント`と省略できますが、本記事では使用しているディレクティブを明確にするため省略記法は使いません。後述するv-bindディレクティブも同様です(`v-bind:属性`を`:属性`と書ける`)。

```vue:Square.vue
<template>
  <button class="square" v-on:click="handleClick">{{ value }}</button>
</template>

<script>
export default {
  data() {
    return {
      value: null,
    };
  },
  methods: {
    handleClick() {
      this.value = "X";
    },
  },
};
</script>
```

### Dataのリフトアップ
#### 高階関数を用いた場合
data(`value`)を個々のSquareコンポーネントではなく、一つのBoardコンポーネントで配列(`squares`)として管理します。そのため、Boardコンポーネントのdataで`squares`、methodsで`handleClick`関数を定義し、Squareコンポーネントはそれらから渡されたpropsで構築します。 
ここにはわかりづらい点がいくつかあります。

- Squareコンポーネントをv-forディレクティブでまとめて生成しています。この際、0から8までの配列を記述する(`i of [0,1,2,3,4,5,6,7,8]`)のが面倒なため、要素数が9の配列を生成し、そのインデックスを取り出して0から8までの配列の代わりにしています。v-bindディレクティブでこの値を`key`の値や`squares`の指定、`handleClick`関数の引数に使用しています。
※`const (_,i) of Array(9)`と書くと正常に動作しないため気をつけましょう。

- `handleClick`関数は引数をもとに`squares`を変更する関数ではなく、**｢`handleClick`の引数をもとに`squares`を変更する無名関数｣**を返り値にもつ高階関数です。そのため各ボタンの`handleClick`にはすべて異なる関数が渡されています。この渡される関数は引数がなく、ボタンのクリック時に実行することで`squares`を変更します。
※返り値の関数は必ずアロー関数で返してください。`function(){処理}`で返した場合、thisがグローバル参照になってエラーが発生します。
 
```js
//以下の無名関数が渡される
// handleClick(1)の場合
() => {
        const squares = this.squares.slice();
        squares[1] = "X"; // ここのインデックスが1
        this.squares = squares;
      };

// handleClick(8)の場合
() => {
        const squares = this.squares.slice();
        squares[8] = "X"; // ここのインデックスが8
        this.squares = squares;
      };
``` 

```vue:Square.vue
<template>
  <button class="square" v-on:click="handleClick">{{ value }}</button>
</template>

<script>
export default {
  props: ["value", "handleClick"],
};
</script>
```
```vue:Board.vue
<template>
  <div class="game-board">
    <div class="status">{{ status }}</div>
    <div class="container">
      <Square
        v-for="(_,i) of Array(9)"
        v-bind:key="i"
        v-bind:value="squares[i]"
        v-bind:handleClick="handleClick(i)"
      ></Square>
    </div>
  </div>
</template>

<script>
import Square from "./Square";

export default {
  components: {
    Square,
  },
  data() {
    return {
      status: "Next player: X",
      squares: Array(9).fill(null),
    };
  },
  methods: {
    handleClick(i) {
      return () => {
        const squares = this.squares.slice();
        squares[i] = "X";
        this.squares = squares;
      };
    },
  },
};
</script>
```

#### イベントバス($emit)を用いた場合(非推奨?)
Vue.jsでは子から親へイベントを伝搬できます。 
しかし、イベント名は自身で管理する必要がある上、双方向の通信になるため規模が大きくなるほどわかりにくくなる恐れがあります。TypeScriptであれば上記の高階関数も親側で型を縛れて安全な上、単方向の通信になるため、基本は関数渡しを使ったほうがいいような気がします。

参考文献:
- [Vue.js $emit 使わないで props で method 渡したほうが良くない？](https://techblog.roxx.co.jp/entry/2020/07/09/092955)
- [Vue.js の event bus (あるいは event hub) 、非推奨になっていた](https://www.ykrods.net/posts/2020/01/29/vuejs-bus-or-hub-has-been-removed/)

```vue:Square.vue
<template>
  <button class="square" v-on:click="handleClick">{{ value }}</button>
</template>

<script>
export default {
  props: ["value"],
  methods: {
    handleClick(){
      this.$emit("click")
    }
  }
};
</script>
```
```vue:Board.vue
<template>
  <div class="game-board">
    <div class="status">{{ status }}</div>
    <div class="container">
      <square
        v-for="(_,i) of Array(9)"
        v-bind:key="i"
        v-bind:value="squares[i]"
        v-on:click="handleClick(i)"
      ></square>
    </div>
  </div>
</template>

<script>
import Square from "./Square";

export default {
  components: {
    Square,
  },
  data() {
    return {
      status: "Next player: X",
      squares: Array(9).fill(null),
    };
  },
  methods: {
    handleClick(i) {
      const squares = this.squares.slice();
      squares[i] = "X";
      this.squares = squares;
    },
  },
};
</script>
```

### 手番の処理
Boaedコンポーネントのdataに`xIsNext`を追加してboolean型でどちらの手番か判断させています。 また、以下のような変更点があります。

- computedに`nextPlayer`を追加することで`xIsNext`の値に応じてプレイヤー名("O"、"X")を自動で変更させ、その値を用いて`status`に表示させる文字列を生成しています。
- `handleClick`関数で生成する関数に`xIsNext`を変更するコードを追加しています。

computedとmethodsはどちらも関数を定義しますが、使用に関して明確な違いがあります。computedは関数で定義しますが、使用する際はpropsやdataと同様に使用できます(`()`が要らない)。また、一度求められた結果はキャッシュされ、dataや親コンポーネントが更新されるまで同じ値を使い回すため計算量を減らせます。一方、methodsは定義したまま通常の関数と同様に使用されます。そのため、propsやdata、他のcomputedから一意的に値が決まる(引数がいらない)関数に関してはcomputed、v-forなどからの引数が必要な関数に関してはmethodsに定義するのが適切でしょう。

```vue:Board.vue
<script>
import Square from "./Square";

export default {
  components: {
    Square,
  },
  data() {
    return {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  },
  computed: {
    nextPlayer() {
      return this.xIsNext ? "X" : "O";
    },
    status() {
      return `Next Player: ${this.nextPlayer}`;
    },
  },
  methods: {
    handleClick(i) {
      return () => {
        const squares = this.squares.slice();
        squares[i] = this.nextPlayer;
        this.squares = squares;
        this.xIsNext = !this.xIsNext;
      };
    },
  },
};
</script>
```

### ゲーム勝者の判定
ゲームの勝敗判定を行う`winner`関数をcomputedに追加しています。これについてはReact側のものとほとんど変わらない(初めに`squares`変数を追加している程度)ため説明は省略します。 
`status`関数で`winner`の値を判定し、有効な値であれば勝敗の表示、無効な値(null)であれば手番の表示するよう変更しています。また、勝敗決定後の値更新や入力済みのマスの上書きを防止するため、`handleClick`内に早期リターンを実装します。

```vue:Board.vue
<script>
import Square from "./Square";

export default {
  components: {
    Square,
  },
  data() {
    return {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  },
  computed: {
    nextPlayer() {
      return this.xIsNext ? "X" : "O";
    },
    winner() {
      const squares = this.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 (const line of lines) {
        const [a, b, c] = line;
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    },
    status() {
      return this.winner
        ? `Winner: ${this.winner}`
        : `Next Player: ${this.nextPlayer}`;
    },
  },
  methods: {
    handleClick(i) {
      return () => {
        const squares = this.squares.slice();
        if (this.winner || this.squares[i]) return;
        squares[i] = this.nextPlayer;
        this.squares = squares;
        this.xIsNext = !this.xIsNext;
      };
    },
  },
};
</script>
```

## タイムトラベル機能の追加
### 着手の履歴の保存 & State のリフトアップ、再び
基本的な考え方はSquareコンポーネントからBoardコンポーネントにリフトアップしたときと同様です。Squareコンポーネントにはpropsで最新の盤面と`handleClick`関数を渡します。以下に注意点を挙げておきます。

- statusはBoardコンポーネントではなく、Gameコンポーネントで管理することにしたため、それぞれのtemplateを編集します。
- GameコンポーネントからBoardコンポーネントに関数を渡す際は、関数を実行せず関数そのものを渡します。
- Gameコンポーネントでは、盤面をdataの`history`で管理します。これはBoardコンポーネントの`squares`の配列で、現在の盤面については`current`で取得します。
- `winner`関数や`handleClick`関数については、初めに`this.current`を`squares`として変数に格納することで書き換え減らしています。
※エディタの機能や置換などでまとめてリファクタリングできるのであれば、直接`this.current`を用いてもいいと思います。

- `handleClick`関数では`this.squares`に代入していた部分を`this.history`に代入するよう変更しています。ここでは`concat`メソッドで配列の結合を行っています。 
※スプレッド構文を用いて`[...this.history, squares]`と書いてもいいと思います。ブラウザや利用状況によって、スプレッド構文と`concat`メソッドのどちらが早いか変わってくるみたいなので難しいところですね･･･

```vue:Board.vue
<template>
  <div class="game-board">
    <div class="container">
      <Square
        v-for="(_,i) of Array(9)"
        v-bind:key="i"
        v-bind:value="squares[i]"
        v-bind:handleClick="handleClick(i)"
      ></Square>
    </div>
  </div>
</template>

<script>
import Square from "./Square";

export default {
  components: {
    Square,
  },
  props: ["squares", "handleClick"],
};
</script>
```
```vue:Game.vue
<template>
  <div class="game">
    <Board v-bind:squares="current" v-bind:handleClick="handleClick"></Board>
    <div class="game-info">
      <div>
        {{ status }}
      </div>
      <ol>
        <!-- TODO -->
      </ol>
    </div>
  </div>
</template>

<script>
import Board from "./Board";

export default {
  components: { Board },
  data() {
    return {
      history: [Array(9).fill(null)],
      xIsNext: true,
    };
  },
  computed: {
    current() {
      return this.history[this.history.length - 1];
    },
    nextPlayer() {
      return this.xIsNext ? "X" : "O";
    },
    winner() {
      const squares = this.current;
      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 (const line of lines) {
        const [a, b, c] = line;
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    },
    status() {
      const winner = this.winner;
      return winner ? `Winner: ${winner}` : `Next Player: ${this.nextPlayer}`;
    },
  },
  methods: {
    handleClick(i) {
      return () => {
        const squares = this.current.slice();
        if (this.winner || squares[i]) return;
        squares[i] = this.nextPlayer;
        this.history = this.history.concat([squares]);
        this.xIsNext = !this.xIsNext;
      };
    },
  },
};
</script>
```

### 過去の着手の表示
数字を引数から受け取って、何番目の手番かボタンに表示する文字列を返す`moveStatus`関数を定義しています。また、li要素をv-forディレクティブで生成します。foreach文で`history`のインデックス(`step`)を受け取り、それを用いて要素を生成します。

```vue:Game.vue
<template>
  <div class="game">
    <Board v-bind:squares="current" v-bind:handleClick="handleClick"></Board>
    <div class="game-info">
      <div>
        {{ status }}
      </div>
      <ol>
        <li v-for="(_,step) of history" :key="i">
          <button>{{ moveStatus(i) }}</button>
        </li>
      </ol>
    </div>
  </div>
</template>

<script>
import Board from "./Board";

export default {
  components: { Board },
  data() {
    return {
      history: [Array(9).fill(null)],
      xIsNext: true,
    };
  },
  computed: {
    current() {
      return this.history[this.history.length - 1];
    },
    nextPlayer() {
      return this.xIsNext ? "X" : "O";
    },
    winner() {
      const squares = this.current;
      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 (const line of lines) {
        const [a, b, c] = line;
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    },
    status() {
      const winner = this.winner;
      return winner ? `Winner: ${winner}` : `Next Player: ${this.nextPlayer}`;
    },
  },
  methods: {
    handleClick(i) {
      return () => {
        const squares = this.current.slice();
        if (this.winner || squares[i]) return;
        squares[i] = this.nextPlayer;
        this.history = this.history.concat([squares]);
        this.xIsNext = !this.xIsNext;
      };
    },
    moveStatus(i) {
      return i ? `Go to move #${i}` : "Go to game start";
    },
    },
  },
};
</script>
```

### タイムトラベルの実装
今何番目か記録するためにdataに`step`を追加し、`current`は`step`を用いて算出します。また、`handleClick`関数は`step`までコピーしてから盤面を更新(追加)し、最後に`step`を増やすよう変更します。

```vue:Game.vue
<template>
  <div class="game">
    <Board v-bind:squares="current" v-bind:handleClick="handleClick"></Board>
    <div class="game-info">
      <div>
        {{ status }}
      </div>
      <ol>
        <li v-for="(_,step) of history" :key="step">
          <button v-on:click="jumpTo(step)">{{ moveStatus(step) }}</button>
        </li>
      </ol>
    </div>
  </div>
</template>

<script>
import Board from "./Board";

export default {
  components: { Board },
  data() {
    return {
      history: [Array(9).fill(null)],
      xIsNext: true,
      step: 0,
    };
  },
  computed: {
    current() {
      return this.history[this.step];
    },
    nextPlayer() {
      return this.xIsNext ? "X" : "O";
    },
    winner() {
      const squares = this.current;
      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 (const line of lines) {
        const [a, b, c] = line;
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    },
    status() {
      const winner = this.winner;
      return winner ? `Winner: ${winner}` : `Next Player: ${this.nextPlayer}`;
    },
  },
  methods: {
    handleClick(i) {
      return () => {
        const squares = this.current.slice();
        if (this.winner || squares[i]) return;
        squares[i] = this.nextPlayer;
        this.history = this.history.slice(0, this.step + 1).concat([squares]);
        this.xIsNext = !this.xIsNext;
        this.step = this.step + 1;
      };
    },
    moveStatus(step) {
      return step ? `Go to move #${step}` : "Go to game start";
    },
    jumpTo(step) {
      this.step = step;
      this.xIsNext = step % 2 === 0;
    },
  },
};
</script>
```

### 完成品
以上でチュートリアル完了です。
[完成品](https://codesandbox.io/s/vue-tutorial-starter-code-forked-mmrsy?file=/src/Game.vue)

## 最後に
Reactの公式チュートリアルでは、最後にこのアプリケーションを発展させるアイディアをいくつか挙げているため、それらを自力で実装していくとスキルがより向上していくと思います。
また、Vue3になりComposition APIという新機能が実装されました。筆者もVue初心者であるため詳しくはないのですが、ReactのHooksのように書けるそうです。この新機能でリファクタリングしていくのも勉強になりそうですね。