![サムネイル](https://d2s4ypph6g1t06.cloudfront.net/img/pc/programming_react-vue_react-vue.avif)
目次
はじめに
ReactとVueはどちらも人気のJavaScriptライブラ/リフレームワークです。
それぞれメリット・デメリットがあり、フロントエンドの技術選定の際にどちらを採用するか悩むことも多いと思います。
この記事では、Reactの公式チュートリアルである「三目並べ」をVueでも書いてみて、ReactとVueのコードを比較してみます。
Reactの公式チュートリアルはこちらです。
![Reactの公式チュートリアル 三目並べ](https://d2s4ypph6g1t06.cloudfront.net/img/pc/programming_react-vue_tic-tac-toe.avif)
Reactの公式チュートリアル 三目並べ
作成したコードについて
チュートリアル内ではJavaScriptは単一ファイルになっていますが、今回は比較しやすくするためにReactもVueもコンポーネントごとにファイルを分けます。
ReactはCodeSandbox、VueはVue SFC Playgroundにコードを置いているので、すぐに動作確認できます。
バージョン
コーディング時のバージョンは以下の通りです。
フレームワーク | バージョン |
---|---|
react | 18.3 |
Vue | 3.4 |
Reactのコード
Vueのコード
コード比較
コンポーネントごとにコードを比較してみます。
App.js/App.vue
App.js/App.vueは、エントリーファイルとなるコンポーネントです。
ゲーム全体で使用する変数もここで定義しています。
Reactコード
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コード
<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ではJSX、VueではHTMLベースのテンプレート構文になっています。
Board.js/Board.vue
Board.js/Board.vueは、三目並べの盤面を定義するコンポーネントです。
Reactコード
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コード
<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はdefineProps
とdefineEmits
という構文で記載する必要があります。
Square.js/Square.vue
Square.js/Square.vueは、三目並べの盤面内のそれぞれのマスを定義するコンポーネントです。
Reactコード
export default function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
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コード
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コード
<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.vue
のcurrentMove
の値が変更されて、過去の手番に戻ります。
上記のコードではどちらも親コンポーネントの関数を子コンポーネントで実行することによって親コンポーネントの変数を変更しています。
Vueの双方向バインディングについて
Vueの場合は双方向バインディングが可能なため、別の記載方法も考えられます。v-model
とdefineModel
を使用すると子コンポーネント側で親コンポーネント側の変数を直接変更できます。
以下がコードの例です。
...
- function jumpTo(nextMove) {
- currentMove.value = nextMove;
- }
...
- <Moves :history="history" @onMoveClick="jumpTo" />
+ <Moves :history="history" v-model="currentMove" />
...
...
- 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もメリットがあるため、プロジェクトやメンバーに応じて選定を行うのが良さそうです。
参考になれば幸いです。