🔰

「ref と reactive」 結局どっちを使えばいいのか? (2024 年最新版)

2024/03/09に公開
1

🏆 結論

とりあえず ref を使えばいい!

おしまいです。ref でできないことはありません。

注意点として補足しておくと、これは「Vue.js が ref を推奨している」「迷ってるならとりあえず ref を使っておけばいい」という話であって、reactive をはじめとする他の Reactivity API が非推奨だという話ではありません。
reactive がケースによって便利であることは Evan 氏なども認めており、そもそも Vue.js には厳密なルールがあるわけではないので、結局は自分の手に馴染むものを選択していくのが良いと思います。
なので、究極的な結論としては「とりあえず ref ファーストで考えておいて、なんらかしらの理由で reactive を使いたいなら別にそれも良い」という話になりますが、特別な理由がない場合は ref の方が推奨されます。

🚩 はじめに

こんにちは! Vue Beginners です。

今回は、Vue ユーザーなら一度は目にしたことがある、もしくは迷ったことがある「ref vs reactive 論争」について解説していきます。

先に断っておきますが、これらは筆者の個人的な考えも含んでますので、他の人が違う意見を持っていることもあると思います。
結論でも言いましたが、究極的には Vue.js にはルールがありません。自分の手に合うものを使うのが一番です。ご参考までに!

📚 ref と reactive って何?

若干のおさらいですが、Vue.js ではステートを定義するときに refreactive といった関数を使います。

template や watcher で参照すると、そのステートが更新された時にエフェクトが実行されます。(※ template についてのエフェクトは内部的な画面の更新関数)

<script setup lang="ts">
const count = ref(0);
function increment() {
  count.value++;
}
</script>

<template>
  <button type="button" @click="increment">Increment</button>
  <p>{{ count }}</p>
</template>
<script setup lang="ts">
const state = reactive({ count: 0 });
function increment() {
  state.count++;
}
</script>

<template>
  <button type="button" @click="increment">Increment</button>
  <p>{{ state.count }}</p>
</template>

https://ja.vuejs.org/api/reactivity-core.html#ref

https://ja.vuejs.org/api/reactivity-core.html#reactive

📖 結局どっちがいいのか?

1. Evan You 氏からの回答 (in Vue Fes Japan 2023)

Vue.js の作者である、Evan You 氏からの回答です。

回答:「ref を使っておけば良いよ

Vue Fes Japan 2023 で、Evan You 氏や、Nuxt の Sebastien Chopin 氏, Daniel Roe 氏に直接質問ができる「Vue.js クリニック」という企画がありました。

ここでは事前に質問を募集し、「いいね」が多い質問から取り上げていくと言うものでしたが、この「ref vs reactive」は最も票を集めた質問でした。

この企画自体は録画などのアーカイブは残っていませんが、筆者もその場で聞いたものなので間違いありません。いくつか、SNS やブログ等での反応もありましたのでそれも参考に載せておきます。

https://x.com/punksy2/status/1718174713955090610?s=20

https://x.com/ruka70663556/status/1718210334413193247?s=20

https://x.com/t0yohei/status/1718173910523580731?s=20

https://x.com/9pid/status/1718174508325236817?s=20

Vue.js の Reactivity API に関して、 ref と reactive は結局どちらを使うのがいいんでしょうか ? 全部 ref で OK と言っている方をちらほら見かけるのですが、逆に reactive はどのようなケースで使うのが良い感じなのでしょうか ?
Evan の回答は以下。
reactive は ref からも呼ばれている。
reactive の書き味は OptionsAPI とも似ているので当初は推していたが、ユーザーが ref に慣れてきた今となっては ref をオススメしたい。
ただ、reactive を使うメリットもあるので、無くすことはない。
さらに追加の理由として、下記を挙げていました。
reactive を使うと一つのオブジェクトに全ての state を突っ込んだような実装になりがちのため。
Options API では this. を使う必要があったので、リアクティブであることがわかりやすい。reactive だと普通の定数なのか、リアクティブな定数なのかが分かりづらい。
Vue3 リリース直後からどちらを使うべきか、意見が分かれる点でしたが、ref を第一に使う、ということで、ついに決着がついたと言えそうです。

引用元: https://zenn.dev/yoriso/articles/83cb3390ee3fc7#ref-vs-reactive

Vue.js に関する質問が事前募集され、Evan Yu などコミッター陣に質問できるセッションでした。 一番人気は「reactive vs ref はどちらを使うべきか」という質問で、 Evan Yu が「ref 統一がおすすめ」とお墨付き をくれたことがありがたかったです。 元々ドキュメント上では reactive が ref より先にありましたが、最近 ref を上にしたのも、ref を使おうというメッセージとのこと。 reactive はデストラクチャでリアクティビティが失われる、など間違いを犯しやすいことが理由の一つですが、 reactive が今後なくなることはないとのこと。 例えば、一回アサインしたらその後変数を置き換えない、2 つの object で共通の state を必ず担保したい、必ずその object があるという状態を担保したい場合などでは有用とのこと(ex, 認証のステータス管理(今ログイン中かどうか)など)

引用元: https://devblog.thebase.in/entry/2023/11/09/160000


ほとんどの場合はこの結論(理由づけ)だけ信じておけば問題なさそうです。(作者が ref を推奨している)

2. 公式ドキュメント

「推奨」 という言及

実は公式ドキュメントでも、ref を推奨しているという旨が書かれています。

それがこちらです。(※ API 選択Composition API にしてご覧ください)

Composition API では、リアクティブな状態を宣言する方法として、ref() 関数を使用することを推奨します:

引用元: https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#declaring-reactive-state-1

reactive() の制限
...
このような制約があるため、リアクティブな状態を宣言するための主要な API として ref() を使用することを推奨します。

引用元: https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive

登場する順番

公式ドキュメントというものは様々なスタイルがありますが、Vue.js のリアクティビティに関するドキュメントは「より、スタンダードでよく使われるもの順」に載っている感じを受けます。
(Vue.js クリニックでも触れられていました)

ドキュメントを見てみると、まず最初に ref が登場し、その次は ref を持ち出して computed が登場します。

https://ja.vuejs.org/api/reactivity-core

そして、その後で reactive が登場します。

ここからも「まずは ref」というメッセージを感じます。

❓ ref が推奨される理由

これまで、Evan 氏の回答や公式ドキュメントをもとに「ref が推奨されている」ということについて見てきましたが、「なぜ ref の方がいいの?」「reactive の存在意義は?」という疑問はまだ残っているかと思いますので、その辺りを考えていきます。

制限

まず、先ほども紹介した公式ドキュメントにもありますが、reactive にはいくつかの制限があります。

リアクティビティーの基礎/reactive()/reactive() の制限

  • プリミティブ値を扱うことができない
  • オブジェクト全体を置換できない

詳しくはドキュメントを参照してください。

reactive は通常のオブジェクトと判別しづらい

たちまち、ref を扱う際に .value が煩わしく感じるかもしれません。

let count = ref(0);
count++;

const state = reactive({ count: 0 });
state.count++;

のように記述した方がスッキリしていて良い感じだと思うこともあるかもしれません。
しかし、リアクティブな値 というのは 反応性を文脈にもつ特別な値であるため、通常はそうでない値との判別は容易であるべきです。
ソースコードが大きくなったときや、他の関数にリアクティブな値を渡したりするときのことを考えましょう。

function mutate(state: { count: number }) {
  // ????
}
// .
// . 長い処理
// .

function mutate() {
  state.count++; // ???
}

部分的に切り出してみると、この state というオブジェクトがただのオブジェクトなのか、リアクティブなオブジェクトなのかがわかりづらいです。

一方で、ref によって作られた値は Ref<T> というオブジェクトになります。

function mutate(countRef: Ref<number>) {
  countRef.value++;
}
// .
// . 長い処理
// .
function mutate() {
  count.value++; // count は Ref<number> であり、.value でアンラップしていることが一目でわかる
}

このように、「何がリアクティブで何がリアクティブでないか」を判別しやすくするためにも、ref の方が良いと言えるでしょう。

先ほど書いた、

let count = ref(0);
count++;

のように .value を省略するような構文の例を挙げましたが、実はこれは過去に Vue でも検討されたことがあります。
それが、「Reactivity Transform」 というものです。

しかしここでも触れたように、境界がわかりづらくなるなどのいくつかの理由で廃棄されました。

https://github.com/vuejs/rfcs/discussions/369#discussioncomment-5059028

Losing .value makes it harder to tell what is being tracked and which line is triggering a reactive effect. This problem is not so noticeable inside small SFCs, but the mental overhead becomes much more noticeable in large codebases, especially if the syntax is also used outside of SFCs.

.value を失うと、何が追跡されているのか、どのラインがリアクティブ効果を引き起こしているのかを知ることが難しくなります。この問題は、小規模な SFC 内ではそれほど顕著ではありませんが、大規模なコードベースでは、特に構文が SFC の外部でも使用されている場合には、精神的なオーバーヘッドがより顕著になります。

また、.value という記述がどうしても気に入らない方はいくつかの工夫を取ることができます。

実はこれも公式ドキュメントで言及されていますので、必要な方はぜひ見て見てください。

https://ja.vuejs.org/guide/extras/reactivity-in-depth#api-design-trade-offs

中でも、大事そうだと思った部分だけ引用しておきます。

これらの API スタイルが自分に合うかどうかは、ある程度主観的なものです。ここでの目標は、これらの異なる API 設計間の根本的な類似性とトレードオフを示すことです。また、Vue は柔軟であり、既存の API に縛られないことも示したいです。必要であれば、より具体的なニーズに合わせて独自のリアクティビティープリミティブ API を作成できます。

❌ よくある誤解 (番外編)

ref vs reactive についての議論で、よくある誤解をいくつか紹介します。

誤解 1. reactive はいらない

これまで ref を推奨してきましたが、reactive はいらないというわけでも非推奨というわけではありません。
文脈に応じて、reactive と ref を使い分けることもできます。

Vue.js クリニックで挙げられていた一つの活用方法として、「Options API からの移植性」とう話しがありました。
歴史的な背景を見てみると、実は reactive の方が、根幹の思想に近いところに存在していたことがわかります。

data オプションと method について考えて見ましょう。

import { defineComponent } from "vue";

export default defineComponent({
  data() {
    return {
      count: 0,
      count2: 0,
      count3: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
      this.count2++;
      this.count3++;
    },
  },
});

Options API 時代、ステートというものは「コンポーネントが持つ一つのリアクティブオブジェクト」であったことがわかります。

イメージとして、

const componentState = reactive(ComponentOptions.data());

let componentMethods;
for (const key in ComponentOptions.methods) {
  componentMethods[key] = ComponentOptions.methods[key].bind(componentState);
}

このような内部イメージです。そしてこの state には this で参照できます。
(※ 実際には data 以外にも、computed, props などが混ざっています)

reactive はこの、「ステートをひとまとまりのオブジェクトとしてみて、this を含めて method も凝集させる」という考え方に近いものです。

実はこんなこともできます。

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

const counter = reactive({
  count: 0,
  count2: 0,
  count3: 0,
  increment() {
    this.count++;
    this.count2++;
    this.count3++;
  },
});
</script>

<template>
  <button @click="counter.increment">Increment</button>
  <p>{{ counter.count }}</p>
  <p>{{ counter.count2 }}</p>
  <p>{{ counter.count3 }}</p>
</template>

このメンタルモデルはかなり Options API に近いです。
Options API で書かれたコンポーネントの一部分(塊)を分けていくことができます。
この需要は今でこそ Composition API に慣れた人が多いために感じづらいですが、いろんなメンタルモデルを受け入れる柔軟性を提供しているという見方もできます。
「なんらかしらのステートをひとまとまりに考えたい」「あわよくばメソッドもそこに凝集させたい」という考え方です。
data, props などの一塊のオブジェクトは実際、内部的にはこうなっていますし、ユーザー側のコードもこの考えで実装している人もいます。

data, props などの一塊のオブジェクトは実際、内部的にはこうなっていますし

https://github.com/vuejs/core/blob/fef2acb2049fce3407dff17fe8af1836b97dfd73/packages/runtime-core/src/componentProps.ts#L218

https://github.com/vuejs/core/blob/fef2acb2049fce3407dff17fe8af1836b97dfd73/packages/runtime-core/src/componentOptions.ts#L728

https://github.com/vuejs/core/blob/fef2acb2049fce3407dff17fe8af1836b97dfd73/packages/runtime-core/src/componentOptions.ts#L690

また、ref で、.value 配下にネストしたオブジェクトが入ってきた場合も内部的に reactive が呼ばれます。
これも考え方は同じで、入力されたひとまとまりのオブジェクトをリアクティブにするためです。

ref の値としてオブジェクトが代入された場合、reactive() でそのオブジェクトは深いリアクティブになります。これはオブジェクトがネストした ref を含む場合、それが深くアンラップされることも意味します。

引用元: https://ja.vuejs.org/api/reactivity-core.html#ref

https://github.com/vuejs/core/blob/fef2acb2049fce3407dff17fe8af1836b97dfd73/packages/reactivity/src/ref.ts#L181

つまり発生を考えるとむしろ、「ステートとして扱うオブジェクトをリアクティブにするために reactive を導入し、プリミティブなレベル "でも" 扱えるように ref というラップ関数を実装した」と捉える方が自然かもしれません。
ひょっとすると、一部のユーザーにとては「いらなくなった」ものかもしれませんが、引き続き使うことは何も問題ありませんし、歴史や Vue の目線で考えてみると、存在意義もあると言えるでしょう。

誤解 2. プリミティブは ref, オブジェクトは reactive

これはやや誤解です。

前述の通り、ref はネストしたオブジェクトもリアクティブにすることができます。

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

interface FormData {
  name: string;
  age: number | null;
}

const formData = ref<FormData>({ name: "", age: null });
function onSubmit() {
  console.log(formData.value);
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <label for="name">Name</label>
    <input id="name" v-model="formData.name" />

    <label for="age">Age</label>
    <input id="age" type="number" v-model.number="formData.age" />

    <button type="submit">Submit</button>
  </form>
</template>

formData の内容はオブジェクトですが、ref を使用して全く問題ありません。(内部で reactive が呼ばれるだけなので)

ref の track/trigger には実は2つポイントがあって、それは、「.value に対するリアクティビティ」と「.value よりネストした部分のリアクティビティ」です。

ref はどちらもリアクティブにします。

前者は、

const count = ref(0);
count.value++;

のように、.value に対する直接的な get/set です。

後者は、

const formData = ref<FormData>({ name: "", age: null });

formData.value.name = "John";
formData.value.age++;

のような、.value よりネストした部分の get/set です。

ref は、value に入ってきた値がプリミティブであればそのまま。オブジェクトであれば内部的に reactive を呼び出しています。
とにかく、どっちも使えるので、プリミティブは ref, オブジェクトは reactive というわけではありません。

これのうち、「.value に対するリアクティビティ」にだけ着目したものが shallowRef です。

const count = shallowRef(0);
count.value++;
const formData = shallowRef<FormData>({ name: "", age: null });
formData.value.name = "John"; // effect(画面の更新) が発生しない....
formData.value.age++; // effect(画面の更新) が発生しない....

formData.value = { name: "John", age: 20 }; // effect(画面の更新) が発生する!!!

https://ja.vuejs.org/api/reactivity-advanced.html#shallowref

それほど使う機会は多くないと思いますが、"value" に着目したリアクティビティというのは正確には shallowRef です。

誤解 3. reactive はリセットできない (工夫の余地あり)

これは誤解というか、概ね正しいですが、工夫で回避することもできるので一応触れます。
工夫すればリセットできないこともないです。

確かに、reactive では、オブジェクト全体を置換できないという制限があります。

const state = reactive({ count: 0 });
state = { count: 1 }; // NG
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // NG

参照: https://ja.vuejs.org/guide/essentials/reactivity-fundamentals#limitations-of-reactive

ですが、工夫をすれば、リセット相当の実装を行うことは可能っちゃ可能です。

以下がその一例です。

interface State {
  count: number;
}

const defaultState = (): State => ({ count: 0 });

const state = reactive(defaultState());

function reset() {
  Object.assign(state, defaultState());
}

これは key が可変しないケースです。
こちらもそれほど使う機会は多くないと思いますが、reactive でリセットできないというのは、「まぁ、工夫すればなんとかなる」というの方が正確かもしれません。

誤解 4. reactive は型不安全だ (工夫の余地あり)

これも誤解というか、概ね正しいですが、工夫で回避することもできるので一応触れます。

前述までの reactive の問題点として、通常のオブジェクトなのか、リアクティブなオブジェクトなのかがわかりづらいという点がありました。
これも工夫で回避することができるっちゃできます。(ほぼやらないですが...)

これは、結局 TypeScript が structural sub-typing であるが為に生まれる問題です。
phantom type などを用いると区別できるというテクニックがあったりはします。

import { type UnwrapNestedRefs, reactive as _reactive } from "vue";

declare const ReactiveMarker: unique symbol;

export type Reactive<T extends object> = UnwrapNestedRefs<T> & {
  [ReactiveMarker]: never;
};

export const reactive = <T extends object>(target: T): Reactive<T> =>
  _reactive(target) as any;
const state = reactive({ count: 0 });
const mutate = (state: Reactive<{ count: number }>) => {
  state.count++;
};
mutate(state);
mutate({ count: 0 }); // error

ややテクニカルなので、ほとんど使う機会はないと思いますが、一応こういうスタイルを考えることもできます。(工夫の余地あり)

🏁 まとめ

refreactive について、Evan 氏の回答や公式ドキュメントをもとに解説してきました。

どっちを使えばいいのか?という問いに対しては、「とりあえず ref を使っておけばいい」という結論になります。
ただし、reactive が非推奨なわけではありませんし、使いたい理由があれば別にそれも良いです。

もし .value の煩わしさを感じる場合や、reactive のいくつかの制約が気になる場合であっても、工夫すれば柔軟に対応できるという点も覚えておきたいポイントです。

推奨は提供しつつも、一つのやり方を強制せず、いろんな書き方をしたいユーザーのニーズに対応できる Vue.js の良さが垣間見えました 😉

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion

たぬきの教祖たぬきの教祖

reactiveの使い所の1つは、将来的にrefに内包されるオブジェクトのリアクティブ化であろう。
そういう使い方をするかは別にしてわかり易い例で言えば、クラスのフィールドをリアクティブに宣言する場合、refで宣言すると、コンストラクタではvalueが必要になる。
一方、その後インスタンス全体をref またはrefである変数に代入すると、元々でrefであったフィールドがreactiveに変更されるためvalueが不要になる。
この何処かで使用方法が変わるという状態を避けるために、クラスのフィールドは元々reactiveで宣言しておくのが良いだろう。