「ref と reactive」 結局どっちを使えばいいのか? (2024 年最新版)
🏆 結論
「とりあえず 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 ではステートを定義するときに ref
や reactive
といった関数を使います。
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>
📖 結局どっちがいいのか?
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 やブログ等での反応もありましたのでそれも参考に載せておきます。
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
が登場します。
そして、その後で 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」 というものです。
しかしここでも触れたように、境界がわかりづらくなるなどのいくつかの理由で廃棄されました。
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
という記述がどうしても気に入らない方はいくつかの工夫を取ることができます。
実はこれも公式ドキュメントで言及されていますので、必要な方はぜひ見て見てください。
中でも、大事そうだと思った部分だけ引用しておきます。
これらの 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 などの一塊のオブジェクトは実際、内部的にはこうなっていますし
また、ref
で、.value
配下にネストしたオブジェクトが入ってきた場合も内部的に reactive
が呼ばれます。
これも考え方は同じで、入力されたひとまとまりのオブジェクトをリアクティブにするためです。
ref の値としてオブジェクトが代入された場合、reactive() でそのオブジェクトは深いリアクティブになります。これはオブジェクトがネストした ref を含む場合、それが深くアンラップされることも意味します。
引用元: https://ja.vuejs.org/api/reactivity-core.html#ref
つまり発生を考えるとむしろ、「ステートとして扱うオブジェクトをリアクティブにするために 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(画面の更新) が発生する!!!
それほど使う機会は多くないと思いますが、"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
ややテクニカルなので、ほとんど使う機会はないと思いますが、一応こういうスタイルを考えることもできます。(工夫の余地あり)
🏁 まとめ
ref
と reactive
について、Evan 氏の回答や公式ドキュメントをもとに解説してきました。
どっちを使えばいいのか?という問いに対しては、「とりあえず ref
を使っておけばいい」という結論になります。
ただし、reactive
が非推奨なわけではありませんし、使いたい理由があれば別にそれも良いです。
もし .value
の煩わしさを感じる場合や、reactive
のいくつかの制約が気になる場合であっても、工夫すれば柔軟に対応できるという点も覚えておきたいポイントです。
推奨は提供しつつも、一つのやり方を強制せず、いろんな書き方をしたいユーザーのニーズに対応できる Vue.js の良さが垣間見えました 😉
Discussion
reactiveの使い所の1つは、将来的にrefに内包されるオブジェクトのリアクティブ化であろう。
そういう使い方をするかは別にしてわかり易い例で言えば、クラスのフィールドをリアクティブに宣言する場合、refで宣言すると、コンストラクタではvalueが必要になる。
一方、その後インスタンス全体をref またはrefである変数に代入すると、元々でrefであったフィールドがreactiveに変更されるためvalueが不要になる。
この何処かで使用方法が変わるという状態を避けるために、クラスのフィールドは元々reactiveで宣言しておくのが良いだろう。