目次
1. はじめに・比較する目的
2. 状態管理とは何か?
3. Vue.jsの状態管理
4. Reactの状態管理(useState)
5. 同じカウンター機能を実装して比較
6. オブジェクト・配列の状態更新の違い
7. 複数コンポーネント間の状態共有
8. それぞれのメリット・デメリット
9. まとめ
1.はじめに
Vue.jsとReactは、どちらもUIを構築するためのJavaScriptライブラリ・フレームワークですが、状態管理の方法には違いがあります
2.状態管理とは何か?
状態(State)とは、アプリケーションが保持する、時間とともに変化するデータのことです。
以下のようなデータが状態に該当します。
- カウンターの数値
- フォームに入力された文字列
- チェックボックスの選択状態
- ログイン中のユーザー情報
通常のJavaScript変数では、値を変更するだけでVue.jsやReactの画面が自動的に更新されるわけではありません。
そこで、Vue.jsではref()やreactive()、ReactではuseState()などの仕組みを使用します。
これらを利用することで、DOMを直接操作する手間を省き、宣言的にUIを記述できます。
ただし、Vue.jsとReactでは、状態の変更を検知してUIを更新する仕組みが異なります。
3. Vue.jsの状態管理(ref / reactive)
refを用いたカウント
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
</script>
<template>
<h1>Vue.js カウント</h1>
<h2>カウント:{{ count }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</template>
Vueでは、ref()でリアクティブな値を作成します。
- JavaScript内で値を読み書きするときは .value を使用する
- テンプレートでは ref が自動的にアンラップされるため、.value は不要
- 値の変更に応じて、依存している画面表示が更新される
refとreactiveの違い
Vue.jsでは、ref()とreactive()を使用してリアクティブな状態を定義できます。
ref()は数値や文字列などのプリミティブ値だけでなく、オブジェクトや配列も扱えます。
reactive()はオブジェクトや配列などのオブジェクト型をリアクティブに扱うためのAPIです。
-
ref: ref()はプリミティブ値だけでなく、オブジェクトや配列も扱えるため、Composition APIにおける基本的な状態管理の方法として利用できます - reactive: reactive()はオブジェクトのプロパティを.valueなしで操作できる特徴があります
| 比較項目 | ref | reactive |
|---|---|---|
| プリミティブ値 | 対応 | 非対応 |
| オブジェクト・配列 | 対応 | 対応 |
| JavaScript内での値の取得 |
.valueが必要 |
プロパティへ直接アクセス |
| 値の更新 |
.value経由 |
プロパティを直接変更 |
| オブジェクト全体の置き換え |
.valueに新しいオブジェクトを代入可能 |
変数を再代入すると元のリアクティブな参照が失われる |
| 分割代入 |
.valueを取り出すとリアクティビティが失われる場合がある |
プリミティブなプロパティの分割代入ではリアクティビティが失われる |
4. Reactの状態管理(useState)
useStateを用いたカウント
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState<number>(0);
const increaseCount = () => {
setCount(count + 1);
};
const decreaseCount = () => {
setCount(count - 1);
};
return (
<>
<h1>React カウント</h1>
<h2>カウント:{count}</h2>
<button onClick={increaseCount}>+1</button>
<button onClick={decreaseCount}>-1</button>
</>
);
}
ReactのuseState
// 現在の値を使用
// 初期値0の場合、次の値は1
const incrementWithValue = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
// 更新関数を使用
// 初期値0の場合、次の値は3
const incrementWithUpdater = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
Reactでは、各レンダリング中のcountはスナップショットとして扱われます。
そのため、同じイベントハンドラー内でsetCount(count + 1)を複数回呼んでも、そのハンドラーが参照しているcount自体は変化しません。
一方、setCount(prev => prev + 1)は、更新キューの直前の状態を使って次の値を計算します。
Reactでは、useState()を使って状態と更新関数を取得します。
- count が現在の状態を表す
- setCount()で次の状態を指定する
- 状態更新によって再レンダリングが要求され、ReactがUIを更新する
5. 同じカウンター機能を比較
どちらもボタンをクリックすることでカウントが増減するという点では同じですが、状態の更新方法には違いがあります。
状態の宣言方法
Vue.js
const count = ref(0);
React
const [count, setCount] = useState(0);
Vue.jsではref()によってリアクティブな値を作成します。
ReactではuseState()によって現在の状態と更新関数を取得します。
状態の更新方法
Vue.js
count.value++;
React
setCount(prev => prev + 1);
Reactの「不変性(Immutability)」の概念
- Reactの状態更新において非常に重要な概念です
- Reactでは、状態を直接変更するのではなく、常に新しいオブジェクトや配列を作成して
setStateに渡す必要があります。これは、Reactが状態の変更を検知するために、参照の比較を行っているためです
避けるべき更新
user.age = 26;
setUser(user);
推奨される更新
setUser(prev => ({
...prev,
age: 26
}));
UI更新の仕組み
Vue.jsでは、リアクティブな値へのアクセスを追跡し、値が変更されたときに、その値に依存するコンポーネントの更新を実行します。
Reactでは、状態の更新関数が呼び出されると、Reactが再レンダリングを要求し、新しい状態を使用してコンポーネントを再実行します。
その後、Reactは必要なDOMの変更を反映します。
つまり、どちらも状態の変更をUIに反映できますが、状態の追跡方法や更新の仕組みが異なります。
6.オブジェクト・配列の状態更新の違い
Vue.jsとReactでは、オブジェクトや配列の状態を更新する方法が異なります。
Vue.jsでは、reactive()を使用することで、オブジェクトのプロパティや配列の要素を直接変更できます。
一方、Reactでは、状態を直接変更せず、新しいオブジェクトや配列を作成して更新することが基本です。
6-1. オブジェクトの更新
Vue.js
<script setup>
import { reactive } from 'vue';
const user = reactive({
name: "太郎",
age: 25
});
const updateAge = () => {
user.age = 26
}
</script>
<template>
<h1>Vue.js reactive</h1>
<h2>ユーザー名:{{ user.name }}</h2>
<h2>年齢:{{ user.age }}</h2>
<button @click="updateAge">年齢更新</button>
</template>
Vue.jsでは、reactive()によって作成したオブジェクトのプロパティを直接変更できます。
user.age = 26と記述するだけで、Vueのリアクティビティシステムが変更を追跡し、必要なUI更新を行います。
React オブジェクト
import { useState } from 'react';
function App() {
const [user, setUser] = useState({
name: '太郎',
age: 25,
});
const handleAgeUpdate = () => {
setUser((prev) => ({
...prev,
age: 26,
}));
};
return (
<>
<h1>React オブジェクト</h1>
<h2>名前:{user.name}</h2>
<h2>年齢:{user.age}</h2>
<button onClick={handleAgeUpdate}>年齢更新</button>
</>
);
}
export default App;
Reactでは、setUser()を使って新しいオブジェクトを渡します。
ここで使用している...prevはスプレッド構文です。
既存のオブジェクトのプロパティを新しいオブジェクトへ展開し、ageだけを上書きしています。
Reactでは状態を読み取り専用として扱うため、user.age = 26のように既存の状態を直接変更することは避けます。
| 比較項目 | Vue.js | React |
|---|---|---|
| オブジェクトの更新 | プロパティを直接変更可能 | 新しいオブジェクトを作成 |
| 配列への追加 |
push()などを使用可能 |
スプレッド構文などで新しい配列を作成 |
| 状態の追跡 | リアクティビティシステム | 状態更新関数による再レンダリング |
| 不変性 | リアクティブなオブジェクトの変更を許容 | 状態を不変として扱う |
6-2.配列の更新
Vue.js
<script setup>
import { ref } from 'vue';
const todos = ref(['読書', '勉強']);
const addTodo = () => {
todos.value.push('運動')
}
</script>
<template>
<h1>Vue.js Todos</h1>
<ul>
<li v-for="(todo, index) in todos" :key="index">
{{ todo }}
</li>
</ul>
<button @click="addTodo">todo追加</button>
</template>
Vue.jsでは、push()などの配列を変更するメソッドを使用できます。
ref()で管理している配列に要素を追加すると、Vueが変更を検知して画面へ反映します。
import { useState } from 'react';
export default function TodoList() {
const [todos, setTodos] = useState<string[]>(['読書', '勉強']);
const addTodo = () => {
setTodos((prev) => [...prev, '運動']);
};
return (
<>
<h1>React Todos</h1>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={addTodo}>Todo追加</button>
</>
);
}
Reactでは、既存の配列にpush()で要素を追加するのではなく、スプレッド構文を使用して新しい配列を作成します。
[...prev, '運動']は、既存の配列の要素を展開した新しい配列に、'運動'を追加する処理です。
今回の例では簡略化のため配列のインデックスをkeyに使用していますが、要素の追加・削除・並べ替えがある実際のTODOアプリでは、各値を識別するために、一意のIDを使用することが推奨されます。
6-3. 比較してわかったこと
Vue.jsはリアクティブなオブジェクトや配列を直接変更できるため、直感的に記述できます。
Reactは新しいオブジェクトや配列を作成して更新する必要がありますが、状態の変更前後を区別しやすいという特徴があります。
ただし、スプレッド構文は浅いコピーであるため、ネストしたオブジェクトを更新する場合には注意が必要です。
7. 複数コンポーネント間の状態共有
実際のアプリケーションでは、1つのコンポーネントだけでなく、複数のコンポーネントで状態を共有することがあります。
例えば、親コンポーネントで管理しているカウンターの値を子コンポーネントに表示し、子コンポーネントからカウントを更新するケースです。
Vue.jsとReactでは、どちらも親から子へpropsを渡せますが、子から親へ更新を伝える方法が異なります。

7-1. Vue.jsのpropsとemit
親コンポーネント(App.vue)
<script setup>
import { ref } from 'vue';
import Counter from './components/Counter.vue';
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
<template>
<Counter :count="count" @increment="increment" />
</template>
子コンポーネント(Counter.vue)
<script setup>
defineProps({
count: Number,
});
const emit = defineEmits(['increment']);
</script>
<template>
<div>
<h2>カウント:{{ count }}</h2>
<button @click="emit('increment')">+1</button>
</div>
</template>
Vue.jsでは、親コンポーネントからpropsで状態を渡し、子コンポーネントからemitでイベントを通知します。
今回の例では、子コンポーネントがincrementイベントを発行し、親コンポーネントがそのイベントを受け取って状態を更新しています。
7-2. Reactのpropsとコールバック関数
親コンポーネント(App.tsx)
import { useState } from 'react';
import Counter from './Counter';
function App() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prev) => prev + 1);
};
return (
<>
<Counter count={count} onIncrement={increment} />
</>
);
}
export default App;
子コンポーネント(Counter.tsx)
type CounterProps = {
count: number;
onIncrement: () => void;
};
export default function Counter({ count, onIncrement }: CounterProps) {
return (
<div>
<h1>React カウント</h1>
<h2>カウント:{count}</h2>
<button onClick={onIncrement}>+1</button>
</div>
);
}
Reactでは、親コンポーネントから状態とコールバック関数をpropsとして渡します。
子コンポーネントは、受け取ったonIncrement関数を実行することで、親コンポーネントの状態更新を要求します。
8.それぞれのメリット・デメリット
ここまで、Vue.jsとReactでカウンターやオブジェクト・配列の更新、コンポーネント間の状態共有を比較しました。
それぞれの状態管理には、次のような特徴があります。
Vue.jsのメリット
1.状態の更新を直感的に記述できる
ref()やreactive()を使用することで、状態を比較的シンプルに更新できます。
特にreactive()では、オブジェクトのプロパティを直接変更できるため、通常のJavaScriptオブジェクトに近い感覚で扱えます。
2.リアクティビティによる依存関係の追跡
Vue.jsはリアクティブな値へのアクセスを追跡し、状態が変更されたときに必要な更新を行います。
Vue.jsの注意点
- refの.valueやリアクティビティの理解が必要
JavaScript内ではrefの値にアクセスする際に.valueが必要ですが、テンプレートでは自動的にアンラップされます。
また、reactive()で作成したオブジェクトの分割代入や置き換えには注意が必要です。
- 状態の変更箇所を把握する必要がある
リアクティブなオブジェクトは直接変更できるため、複数の処理から同じ状態を変更する設計では、どこで値が変化したかを追跡しにくくなる場合があります。
Reactのメリット
1.状態の更新が明示的
useState()では、更新関数を使用して状態を変更します。
そのため、どこで状態更新が要求されているのかをコードから把握しやすい特徴があります。
2.イミュータビリティを重視した設計
Reactでは状態を不変として扱い、新しいオブジェクトや配列を作成して更新します。
これにより、状態の変更前後を区別しやすくなり、予測可能な状態更新につながります。
Reactの注意点
1.オブジェクトや配列の更新に慣れが必要
Reactでは、オブジェクトや配列を更新する際に、スプレッド構文やmap()、filter()などを使用することが多くなります。
特にネストしたオブジェクトでは、更新処理が複雑になる場合があります。
2.再レンダリングとstateのスナップショットの理解が必要
Reactでは、状態の更新が即座に現在の変数を書き換えるわけではありません。
そのため、更新処理を正しく記述するには、再レンダリングや状態更新のキューについて理解する必要があります。
| 比較項目 | Vue.js | React |
|---|---|---|
| 基本的な状態管理 |
ref() / reactive()
|
useState() |
| 状態更新の記述 | 直接変更できる | 更新関数を使用 |
| オブジェクト更新 | 比較的簡潔 | 不変性を意識する必要がある |
| 状態の追跡 | リアクティビティによる依存関係の追跡 | 状態更新による再レンダリング |
| 学習時の注意点 |
.value、Proxy、分割代入 |
イミュータビリティ、スナップショット |
| コンポーネント間の共有 | props / emit | props / コールバック |
どちらを選ぶべきか?
今回のような小規模なカウンターやTODOリストであれば、どちらも比較的簡単に実装できます。
Vue.jsは、リアクティブな値を直接変更できるため、状態更新の記述が簡潔になりやすいです。
一方、Reactは、状態の更新を明示的に記述し、イミュータビリティを重視するという特徴があります。
どちらが優れているというわけではなく、既存の技術構成やチームの経験、アプリケーションの要件などを踏まえて選択することが重要だと考えます。
9.まとめ
本記事では、Vue.jsのComposition APIとReact Hooksを使用して、状態管理の方法を比較しました。
今回の比較を通して、以下の違いを整理できました。
-
Vue.jsではref()やreactive()を使用し、リアクティブな状態を管理する
-
ReactではuseState()を使用し、更新関数を通して状態を変更する
-
Vue.jsではリアクティブなオブジェクトや配列を直接変更できる
-
Reactではイミュータビリティを意識して、新しいオブジェクトや配列を作成する
-
コンポーネント間の状態共有では、Vue.jsのpropsとemit、Reactのpropsとコールバック関数などを利用できる
-
どちらが優れているというわけではなく、プロジェクトの要件やチームの技術構成に応じて選択される形かと思います
-
また、今回取り上げたのは基本的な状態管理ですが、アプリケーションの規模が大きくなると、Vue.jsではPinia、ReactではRedux Toolkitなどのライブラリも選択肢になります
今後は、こうした状態管理ライブラリの違いについても学習していきたいと思います。
