【Vue】refとreactiveの違い:Vueのリアクティブシステム入門

JavaScript

こんにちは、かつコーチです。

前回は記法対応表でuseStaterefの対応を軽く紹介しました。

今回はそのrefをもう少し深掘りしつつ、もう一つの選択肢であるreactiveとの違いを整理していきます。

「結局どっちを使えばいいの?」という疑問に、私なりの判断軸も含めて答えていきます。

リアクティブとは何か

値の変化に画面が自動で追従する仕組み

Vueにおけるリアクティブとは、変数の値が変化したときに、その変数を使っている画面表示が自動的に更新される仕組みのことです。

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);
</script>

<template>
  <p>カウント:{{ count }}</p>
  <button @click="count++">+1</button>
</template>

ボタンを押してcountの値を増やすと、画面上の「カウント:◯」の表示も自動で更新されます。

普通のJavaScriptの変数であれば、値を変えても画面には何も反映されません。

Vueは、refreactiveで作られた値の変化を裏側で検知し、関連する画面表示を自動で再描画してくれます。

ReactのuseStateも「状態が変わると再レンダリングされる」という点では似ていますが、ReactはsetCountという更新関数を呼ぶことで明示的に再レンダリングをトリガーするのに対し、Vueは値そのものへの変更を自動的に検知する、という仕組みの違いがあります。

ref:どんな型の値も扱える基本形

基本の書き方

refは、文字列・数値・真偽値はもちろん、オブジェクトや配列まで、どんな型の値もリアクティブにできる関数です。

<script setup lang="ts">
import { ref } from "vue";

const count = ref<number>(0);
const message = ref<string>("こんにちは");
const isVisible = ref<boolean>(true);
</script>

refで作った値は、.valueというプロパティ経由で読み書きします。

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);

const increment = () => {
  count.value++; // スクリプト内では .value が必要
};
</script>

<template>
  <!-- テンプレート内では .value は不要 -->
  <p>{{ count }}</p>
  <button @click="increment">+1</button>
</template>

なぜ.valueが必要なのか

JavaScriptの通常の変数は、再代入しても「変数の中身が変わった」ことを外から検知する方法がありません。

そこでVueは、refで値を「オブジェクトの中に包む」ことで、値の変化を検知できるようにしています。

// refの中身は、ざっくりこんなイメージのオブジェクト
{
  value: 0,
  // valueへの代入をVueが裏側で監視している
}

スクリプト内では、この「包み」を意識して.valueを付ける必要がありますが、テンプレート内ではVueが自動的に.valueを補ってくれるため、{{ count }}とだけ書けば動きます。

reactive:オブジェクトをまるごとリアクティブにする

基本の書き方

reactiveは、オブジェクトや配列専用のリアクティブ化の関数です。

<script setup lang="ts">
import { reactive } from "vue";

type UserForm = {
  name: string;
  age: number;
};

const form = reactive<UserForm>({
  name: "",
  age: 0,
});

const updateName = (newName: string) => {
  form.name = newName; // .value不要で直接プロパティを更新できる
};
</script>

<template>
  <p>名前:{{ form.name }}</p>
  <input @input="updateName(($event.target as HTMLInputElement).value)" />
</template>

reactiveで作ったオブジェクトは、.valueを経由せず、通常のオブジェクトと同じようにform.nameのようにプロパティへ直接アクセスできます。

「フォームの入力値をまとめて管理したい」といった、複数のプロパティを持つデータを扱う場面で書き心地が良い方法です。

refとreactiveの使い分け

実務でよく使われる判断軸

私が現場で意識している使い分けの基準を整理すると、次のようになります。

観点refreactive
扱える型プリミティブ値・オブジェクト・配列すべてオブジェクト・配列のみ
アクセス方法.valueが必要(テンプレート内は不要)直接プロパティにアクセス
再代入count.value = 別の値で丸ごと差し替え可能オブジェクト自体の差し替えは不可(リアクティブ性が切れる)
使いどころカウンター・フラグなど単一の値フォームの入力値など複数プロパティのまとまり

結論から言うと、基本はrefを使い、複数の関連するプロパティをまとめて扱いたいときだけreactiveを検討する、というのが私のおすすめです。

Vue公式のスタイルガイドでも、refを基本としつつreactiveを必要に応じて使う、という方針が案内されています。

reactiveの落とし穴:分割代入で壊れる

reactiveには、知らないと必ず一度はハマる落とし穴があります。

❌ Before:reactiveのオブジェクトを分割代入してしまう

<script setup lang="ts">
import { reactive } from "vue";

const form = reactive({ name: "", age: 0 });

// 分割代入すると、リアクティブ性が失われる
const { name, age } = form;

const updateName = () => {
  // nameを直接書き換えても、画面には反映されない
  // (この時点でnameはただの文字列のコピーになっている)
};
</script>

reactiveのオブジェクトから値を分割代入すると、その時点で「値のコピー」が取り出されるだけになり、元のformとのつながり(リアクティブ性)が切れてしまいます。

私自身、このバグに最初に遭遇したときは、「フォームの値を書き換えているはずなのに、画面が全く更新されない」という現象に小一時間悩まされました。

コンソールでformの中身を確認すると値は正しく更新されているのに、画面だけが反映されない、という状態で、原因の見当がつかず苦労した記憶があります。

✅ After:toRefsを使うか、form.nameのまま扱う

<script setup lang="ts">
import { reactive, toRefs } from "vue";

const form = reactive({ name: "", age: 0 });

// toRefsを使うと、リアクティブ性を保ったまま分割代入できる
const { name, age } = toRefs(form);

const updateName = (newName: string) => {
  name.value = newName; // toRefs経由なのでこの更新はform.nameにも反映される
};
</script>

<template>
  <p>{{ name }}({{ age }}歳)</p>
</template>

分割代入がどうしても必要な場合は、toRefsという専用の関数を使うことで、それぞれのプロパティをrefとして取り出せます。

迷った場合は、無理に分割代入せずform.nameのまま扱う方がシンプルで、このつまずきを未然に防げます。

まとめ

この記事のポイント

  • リアクティブとは、値の変化に画面表示が自動で追従する仕組み
  • refはどんな型でも扱え、スクリプト内では.value経由でアクセスする
  • reactiveはオブジェクト専用で、.valueなしで直接プロパティにアクセスできる
  • 基本はrefを使い、複数プロパティのまとまりを扱うときだけreactiveを検討する
  • reactiveのオブジェクトを分割代入するとリアクティブ性が切れるため、toRefsを使うか分割代入自体を避ける

次に読むべき記事

{{ }}の中には簡単な式しか書けない、という話を以前しましたが、複雑な計算やフィルタリングを行いたいときはどうすればいいのでしょうか。

次回は、そんなときに使う「算出プロパティ(computed)」を解説します。

→ 次の記事:computedで算出プロパティを使いこなす

タイトルとURLをコピーしました