React公式チュートリアルをVueでも書いて比較してみた

サムネイル
※当ブログの記事にはアフィリエイトリンクを使用している場合があります。

はじめに

ReactVueはどちらも人気のJavaScriptライブラ/リフレームワークです。
それぞれメリット・デメリットがあり、フロントエンドの技術選定の際にどちらを採用するか悩むことも多いと思います。

この記事では、Reactの公式チュートリアルである「三目並べ」をVueでも書いてみて、ReactとVueのコードを比較してみます。

Reactの公式チュートリアルはこちらです。

Reactの公式チュートリアル 三目並べ

Reactの公式チュートリアル 三目並べ

作成したコードについて

チュートリアル内ではJavaScriptは単一ファイルになっていますが、今回は比較しやすくするためにReactもVueもコンポーネントごとにファイルを分けます

ReactはCodeSandbox、VueはVue SFC Playgroundにコードを置いているので、すぐに動作確認できます。

バージョン

コーディング時のバージョンは以下の通りです。

フレームワークバージョン
react18.3
Vue3.4

Reactのコード

CodeSandbox

Vueのコード

Vue SFC Playground

コード比較

コンポーネントごとにコードを比較してみます。

App.js/App.vue

App.js/App.vueは、エントリーファイルとなるコンポーネントです。
ゲーム全体で使用する変数もここで定義しています。

Reactコード

App.js
import { useState } from "react";
import Board from "./Board";
import Moves from "./Moves";

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>
          <Moves history={history} onMoveClick={jumpTo} />
        </ol>
      </div>
    </div>
  );
}

Vueコード

App.vue
<script setup>
  import { ref, computed } from "vue";
  import Board from "./Board.vue";
  import Moves from "./Moves.vue";

  const history = ref([Array(9).fill(null)]);
  const currentMove = ref(0);
  const xIsNext = computed(() => {
    return currentMove.value % 2 === 0;
  });
  const currentSquares = computed(() => {
    return history.value[currentMove.value];
  });

  function handlePlay(nextSquares) {
    const nextHistory = [...history.value.slice(0, currentMove.value + 1), nextSquares];
    history.value = nextHistory;
    currentMove.value = nextHistory.length - 1;
  }

  function jumpTo(nextMove) {
    currentMove.value = nextMove;
  }
</script>

<template>
  <div className="game">
    <div className="game-board">
      <Board :xIsNext="xIsNext" :squares="currentSquares" @onPlay="handlePlay" />
    </div>
    <div className="game-info">
      <Moves :history="history" @onMoveClick="jumpTo" />
    </div>
  </div>
</template>

ReactでuseStateを使用している変数は、Vueではrefを使用して定義しています。
また、計算して算出される値はVueではcomputedを使用して定義しています。

HTMLの記述はReactではJSXVueではHTMLベースのテンプレート構文になっています。

Board.js/Board.vue

Board.js/Board.vueは、三目並べの盤面を定義するコンポーネントです。

Reactコード

Board.js
import Square from "./Square";

export default function Board({ xIsNext, squares, onPlay }) {
  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

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

  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    onPlay(nextSquares);
  }

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

Vueコード

Board.vue
<script setup>
  import { computed } from "vue";
  import Square from "./Square.vue";

  const props = defineProps({
    xIsNext: Boolean,
    squares: Array,
  });

  const emit = defineEmits(["onPlay"]);

  const winner = computed(() => calculateWinner(props.squares));

  const status = computed(() => {
    if (winner.value) {
      return "Winner: " + winner.value;
    } else {
      return "Next player: " + (props.xIsNext ? "X" : "O");
    }
  });

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

  function handleClick(i) {
    if (winner.value || props.squares[i]) {
      return;
    }
    const nextSquares = props.squares.slice();
    if (props.xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    emit("onPlay", nextSquares);
  }
</script>

<template>
  <div class="status">{{ status }}</div>
  <div class="board-row">
    <Square :value="props.squares[0]" @onSquareClick="() => handleClick(0)" />
    <Square :value="props.squares[1]" @onSquareClick="() => handleClick(1)" />
    <Square :value="props.squares[2]" @onSquareClick="() => handleClick(2)" />
  </div>
  <div class="board-row">
    <Square :value="props.squares[3]" @onSquareClick="() => handleClick(3)" />
    <Square :value="props.squares[4]" @onSquareClick="() => handleClick(4)" />
    <Square :value="props.squares[5]" @onSquareClick="() => handleClick(5)" />
  </div>
  <div class="board-row">
    <Square :value="props.squares[6]" @onSquareClick="() => handleClick(6)" />
    <Square :value="props.squares[7]" @onSquareClick="() => handleClick(7)" />
    <Square :value="props.squares[8]" @onSquareClick="() => handleClick(8)" />
  </div>
</template>

コンポーネントの引数は、ReactではJavaScriptの関数と同じように記載できますが、VueはdefinePropsdefineEmitsという構文で記載する必要があります。

Square.js/Square.vue

Square.js/Square.vueは、三目並べの盤面内のそれぞれのマスを定義するコンポーネントです。

Reactコード

Square.js
export default function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

Vueコード

Square.vue
<script setup>
  const props = defineProps({
    value: String,
  });

  const emit = defineEmits(["onSquareClick"]);
</script>

<template>
  <button className="square" @click="emit('onSquareClick')">{{ props.value }}</button>
</template>

こちらもコンポーネントの引数の定義方法に差があります。

Moves.js/Moves.vue

Moves.js/Moves.vueは、ゲーム履歴の表示と履歴へ遷移するためのボタンを表示するコンポーネントです。

Reactコード

Moves.js
export default function Moves({ history, onMoveClick }) {
  return history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = "Go to move #" + move;
    } else {
      description = "Go to game start";
    }
    return (
      <li key={move}>
        <button onClick={() => onMoveClick(move)}>{description}</button>
      </li>
    );
  });
}

Vueコード

Moves.vue
<script setup>
  import { computed } from "vue";

  const props = defineProps({
    history: Array,
  });

  const emit = defineEmits(["onMoveClick"]);

  const moves = computed(() => {
    return props.history.map((squares, move) => {
      let description;
      if (move > 0) {
        description = "Go to move #" + move;
      } else {
        description = "Go to game start";
      }
      return { description, move };
    });
  });
</script>

<template>
  <ul>
    <li v-for="({ description, move }, index) in moves" :key="index">
      <button @click="emit('onMoveClick', move)">{{ description }}</button>
    </li>
  </ul>
</template>

Reactは任意の位置でJSXを記載できるため、簡潔なコードになっています。

Moves.js/Moves.vue内のボタンをクリックすると、App.js/App.vuecurrentMoveの値が変更されて、過去の手番に戻ります。
上記のコードではどちらも親コンポーネントの関数を子コンポーネントで実行することによって親コンポーネントの変数を変更しています。

Vueの双方向バインディングについて

Vueの場合は双方向バインディングが可能なため、別の記載方法も考えられます。
v-modeldefineModelを使用すると子コンポーネント側で親コンポーネント側の変数を直接変更できます。

以下がコードの例です。

App.vue
...
- function jumpTo(nextMove) {
-   currentMove.value = nextMove;
- }
...
-         <Moves :history="history" @onMoveClick="jumpTo" />
+         <Moves :history="history" v-model="currentMove" />
...
Moves.vue
...
- const emit = defineEmits(['onMoveClick'])
+ const currentMove = defineModel()
...
+ function onClick(nextMove) {
+   currentMove.value = nextMove;
+ }
...
-       <button @click="emit('onMoveClick', move)">
+       <button @click="onClick(move)">
...

双方向バインディングはメリット・デメリットがあるため、実際に使うかは検討が必要です。

React/Vueのコードの違いを考察

ReactとVueのコードを比較して分かった両者の特徴を記載してみます。

React

Reactの特徴

  • コンポーネントの引数は関数の引数としてシンプルに記述できる
  • JavaScript内の任意の箇所でHTML(JSX)を記載できる

Vue

Vueの特徴

  • JavaScriptとHTMLの境界が分かりやすい
  • HTMLのテンプレート構文がHTMLに似ているため理解しやすい

コードを比較してみると、ReactはJavaScriptが軸となっており、VueはHTMLを軸としている印象を受けました。
プロジェクトやメンバーの技術スタックが、JavaScriptとHTMLどちらに寄っているかも選定の基準の一つとするのもありかもしれません。

まとめ

この記事では、Reactの公式チュートリアルである「三目並べ」をVueでも書いてみて、ReactとVueのコードを比較しました。
ReactもVueもメリットがあるため、プロジェクトやメンバーに応じて選定を行うのが良さそうです。

参考になれば幸いです。

この記事をSNSで共有する

Amazon ポイントアップキャンペーン

7/11(木)7/17(水)

最大15%ポイントアップ

キャンペーンにエントリー

関連記事

オススメ記事

このブログを運営している人

アイコン

Webシステムエンジニア / DTMer

あっしゅからー

フリーランスでWebシステムエンジニアをやっています。

趣味ではDTMをしていて、オリジナル曲をニコニコ動画やYouTubeに投稿しています。

Copyright © 2024 あっしゅからー - All right reserved