この記事は、Vue #2 Advent Calendar 2019 の18日目の記事です!!
※ 2021.10追記
ref, reactiveについては公式ドキュメントなどをご参考にいただけますと幸いです。
https://v3.ja.vuejs.org/guide/reactivity-fundamentals.html
(本記事はご参考程度にお願い致します。)
前置き
Vueにそこまで精通しているわけではないため、内容に誤りがある可能性があります。
何か間違いがある場合はお手柔らかにご指摘お願いしますmm
また、多くの方々の記事を参考にさせて頂きました。
ありがとうございますmm
(もしも載せてはいけない情報がある場合、ご連絡頂ければと思います。)
結論
お忙しい方々向けに。
-
ref
は、プリミティブな(Objectでない)値をリアクティブにする -
reactive
はObjectの値をリアクティブにする -
reactive
に含まれる一部のプロパティの値をリアクティブにしたい場合、toRefs()
を使用する -
ref
でもreactive
でも型推論ちゃんと効くよ - 使い分けのベストプラクティスはまだ存在していない(ケースバイケースとしか)
概要
Vue3への期待が高まっていく中、新機能などが取り上げられています。
その代表的な機能に、なんと。
composition-apiというものがあります。(今更)
このアドベントカレンダー然り、その他各所でcomposition-apiが話題になっています。
自分もそれに乗じます。
composition-apiを使うと何が嬉しいかというと、composition apiそのものが導入された動機にも記載されている通り、以下の二点が挙げられています。
- Logic Reuse & Code Organization (ロジックの再利用性とコード編成)
- Better Type Inference (型推論の改善)
詳細は上記のドキュメントや、他記事などでも解説をされているので、そちらをご覧頂ければと思います。
https://qiita.com/hareku/items/0cdaea2ac82aa3a355b6
従来のVueにおける機能(data
, props
, computed
, methods
など)において、
例えば、リアクティブなデータ/値を実現するために必要なdata
プロパティをcomposition-apiで実現する際には、2つのAPIを利用することが手段として考えられます。
ref
とreactive
です。
これらのAPIについて、
- 機能や特性の違いを確認しつつ
- どのように使い分けると良いのか
公式ドキュメントを読みつつ考えてみようと思います。
動作確認について
環境はMac上で、Vue CLIを用いて準備します。
以下を参考にさせていただきました。
先取りVue 3.x !! Composition API を試してみる - 環境構築
リアクティブなデータ/値って何?
Vue従来のdata
であったり、composition apiのref
とreactive
においては、リアクティブなデータ/値を実現するために必要な機能です。
では、そもそもリアクティブという言葉の定義について、知っておく必要があると思います。
私の言葉で説明するよりも、他のアドベントカレンダーの記事で紹介されている記事があるので、こちらを...↓
きたるべきvue-nextのコアを理解する - そもそも「リアクティブ」とは?
ある変数を書き換えた時に、事前に定めた関係性を元に、他の変数が適切に更新されたり、事前に定めた動作が発動することを「リアクティブである」と言います
こちらの記載も参考にした上で、それではリアクティブなデータ/値は何か、というと、ものすごく簡潔に言ってしまえば、
「他のある変数を書き換えた時に、ある関係性によって更新されるデータ/値」
が、リアクティブなデータ/値ということになると思います。
composition apiにおけるref
とreactive
の違いとは何か
いよいよ本題です。
公式ドキュメントの以下の章を読み解きます。
公式ドキュメント#ref-vs-reactive
公式ドキュメント#overhead-of-introducing-refs
ref
とreactive
の違いについてざっくり理解する
ref
においても、reactive
においても、リアクティブなデータ/値を実現できる機能であるという点から、
「どちらを使ってもユーザーの課題は解決できそう」, 「どちらも便利〜〜〜」という (小並感) たっぷりの感想を抱いたわけですが、それぞれの違いについても知っておかなければなりません。(戒め)
ざっくりと理解するために、まずは公式の以下を確認しました。
The difference between using ref and reactive can be somewhat compared to how you would write standard JavaScript logic:
(コード記載部分は省略)
・If using ref, we are largely translating style (1) to a more verbose equivalent using refs (in order to make the primitive values reactive).
・Using reactive is nearly identical to style (2). We only need to create the object with reactive and that's it.
上記から、ref
とreactive
に関して、多少雑ではありますが以下のような理解ができるかと思います。
- ref = プリミティブな値(Object(オブジェクト)ではない値)をリアクティブにしているもの
- reactive = Object(オブジェクト)をリアクティブにしているもの
それぞれの機能が便利であるとともに、どういった機能を持っているのかをものすごくざっくり確認できたところで、それぞれの問題点についても記載されているので、そちらも見ていきます。
ref
を扱う際の特性/問題点
公式ドキュメント#overhead-of-introducing-refs から、ref
そのものは、composition apiの提案に伴う新しい概念だと説明されています。
(reactive
は、こちらでも説明されてますが、Vue.observable()
と同等の機能とされているため)
それによって、ref
には以下のような欠点があるとされています。
- When using the Composition API, we will need to constantly distinguish refs from plain values and objects, increasing the mental burden when working with the API.
- Reading and mutating refs are more verbose than working with plain values due to the need for .value.
1については、概念的に新しいものを扱うことになるため、単純なオブジェクトや値などと区別する必要があるとされています。
(そのオブジェクト/変数が、ref
としての機能を持っているかどうかを見極める必要があるからかと思います。)
説明の中では、変数名の後にRef
をつけてあげるなどして、区別してあげたほうが良いという記載もありますね。
// シンプルな変数の場合
const num = 0
// refの機能を用いた場合
const numRef = ref(0)
2については、ref
で定義された値を読み取るためには、 .value
で読み取ってあげる必要があるとのことです。確かに冗長に感じてしまうかもしれませんね。
// refの機能を用いたnumを定義
const numRef = ref(0)
// 上記のvalue(=0)を利用するためには、`.value`を用いる必要がある。
function increment() {
numRef++ // この記載だとNG
numRef.value++ // こちらはOK
}
その後に、以下のように「ref
の概念を使わず、リアクティブオブジェクトだけでどうにかできないか」というところを説明しています。
We have discussed whether it is possible to completely avoid the Ref concept and use only reactive objects
こちらについては、以下のように説明してあり、ref
を避けることは難しそうだという印象を受けます。
・Computed getters can return primitive types, so a Ref-like container is unavoidable.
=> computedの機能を用いた場合、プリミティブな値を返すことができるため、リアクティブなデータを用いたい場合にはref
は避けられない、とされています。(=reactive
では同等のことは実現できないという認識)
・Composition functions expecting or returning only primitive types also need to wrap the value in an object just for reactivity's sake. It's very likely that users will end up inventing their own Ref like patterns (and causing ecosystem fragmentation) if there is not a standard implementation provided by the framework.
=> プリミティブ型の返却を期待する関数がある場合、その返却する値にリアクティビティーを持たせるためにrefが必要と言っている? (ここはまだ腑に落ちていない&理解できていないです。。。私自身の残課題です)
reactive
を扱う際の特性/問題点
reactive
を扱う際は以下のような問題があるとされています。
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:
要は、reactive
を用いたリアクティブなデータについては、そのデータオブジェクト自体への参照を保持する必要があり、
reactive
なデータに含まれるプロパティを分割して取り出して扱うことができないということです。
(厳密には、reactive
なデータに含まれるプロパティを分割して取り出すことは可能ですが、そのプロパティはリアクティブな状態を持ってない、ということになります。)
公式でもコードの参考例を用いて説明してありますが、私なりの解釈も交えつつ以下に記載してみます。
reactive
を用いたソースコードを実装してみる
ここで確認したいのは、reactive
で定義したオブジェクトの利用方法によって、リアクティブな状態がロストしているかどうかです。
例として、stateオブジェクトがcountを持っており、そのcountをマイナス, プラスするだけのアプリケーションを作成します。
まず、reactiveで定義したオブジェクトが以下です。
import { reactive } from '@vue/composition-api'
export function useCountReactive() {
const state = reactive({
count: 0
})
return state
}
上記をアプリケーション側で利用しますが、2パターン用意します。
- Aパターン
- stateオブジェクトそのものを利用するパターン
- Bパターン
- stateオブジェクトが含むプロパティ(count)を利用するパターン
<template>
<div id="app">
<div>
<!-- Aパターン -->
<h1>Aパターン</h1>
<button @click="decrementA()">-</button>
{{ state.count }}
<button @click="incrementA()">+</button>
<!-- Bパターン -->
<h1>Bパターン</h1>
<button @click="decrementB()">-</button>
{{ count }}
<button @click="incrementB()">+</button>
</div>
</div>
</template>
<script lang="ts">
import { useCountReactive } from './compositions/CountReactive'
export default {
setup() {
// Aパターン
const state = useCountReactive()
function incrementA() {
state.count++
}
function decrementA() {
state.count--
}
// Bパターン (countそのものに変更を加えたい想定なので、letで定義しています)
let { count } = useCountReactive()
function incrementB() {
count++
}
function decrementB() {
count--
}
return {
state,
incrementA,
decrementA,
count,
incrementB,
decrementB
}
}
}
</script>
このようにした場合の挙動を確認します。
Aパターンだと、カウントの増減は確認できましたが、
Bパターンは0から変動がありません。
このように、Bパターンの場合、リアクティブ状態が損失されていることが確認できました。
reactive
なオブジェクトのプロパティも、リアクティブなデータとして扱いたい場合
公式に書いてある通り、toRefs()
でreactive
で定義したオブジェクトをreturnしてあげるだけで良いそうです。
- import { reactive } from '@vue/composition-api'
+ import { reactive, toRefs } from '@vue/composition-api'
export function useCountReactive() {
const state = reactive({
count: 0
})
- return state
+ return toRefs(state)
}
そうすると、count
プロパティが、ref
で定義されているのと同じ状態になります。
そのため、アプリケーション側のソースコードにも一部修正が必要です。
:App.vue
<template>
.
.
<!-- Aパターン -->
<h1>Aパターン</h1>
<button @click="decrementA()">-</button>
- {{ state.count }}
+ {{ state.count.value }}
.
.
</template>
<script lang="ts">
import { useCountReactive } from './compositions/CountReactive'
export default {
setup() {
// Aパターン
const state = useCountReactive()
function incrementA() {
- state.count++
+ state.count.value++
}
function decrementA() {
- state.count--
+ state.count.value--
}
// Bパターン (countそのものに変更を加えたい想定なので、letで定義しています)
let { count } = useCountReactive()
function incrementB() {
- count++
+ count.value++
}
function decrementB() {
- count--
+ count.value--
}
.
.
}
}
</script>
こうすることでいずれのパターンでも値が変更できていることが確認できます。
(GIFアニメじゃなく申し訳ありませんが)
いずれのパターンでもカウントの増減を確認できています。
そういえばref
とreactive
の型推論ってちゃんと効いてる?
結論から言うと、どちらも型推論はしっかりできています。
以下のようなモジュールを作ってみました。
import { reactive, ref } from '@vue/composition-api'
export function useCountObject() {
const state = reactive({
count: 0
})
const countRef = ref(0)
return {
state,
countRef
}
}
stateは、number型の値を持つと推論されるcountを持っており、
countRefはnumber型のRef interfaceを持つと推論されるはずです。
利用側で確認してみましょう。
// script部分の一部のみ
import { useCountObject } from './compositions/CountObject'
export default {
setup() {
const { state, countRef } = useCountObject()
.
.
エディタ上で、state, countRefについて確認してみます。
じゃあ結局のところ、ref
とreactive
をどう使い分けるのか
「リアクティブなデータ/値って何?」でも記載した通り、ref
とreactive
は、リアクティブなデータ/値を実現するために必要な機能です。
公式のページにも見解が記載されています。
公式見解
今後は使い分け方をどうするかを知っておきたいですが、公式が以下のような見解を出しています。
1.Use ref and reactive just like how you'd declare primitive type variables and object variables in normal JavaScript. It is recommended to use a type system with IDE support when using this style.
2.Use reactive whenever you can, and remember to use toRefs when returning reactive objects from composition functions. This reduces the mental overhead of refs but does not eliminate the need to be familiar with the concept.
要は、なるべくreactive
を使うようにしながら、toRefs()
を使うようにすれば、精神的負担も少ないのではないかとのこと。
また、以下のようにも記載しています。
At this stage, we believe it is too early to mandate a best practice on ref vs. reactive. We recommend you to go with the style that aligns with your mental model better from the two options above. We will be collecting real world user feedback and eventually provide more definitive guidance on this topic.
ベストプラクティスを今決めるのは時期尚早だ、とのことです。
そもそもまだ流通していないので、Vue3が待たれるこれから、またVue3がリリースされてから、その点は議論されていくのでしょう。
私個人の見解
ここまで調べておいて何ですが、私も「まだまだ導入が進んでからでないと...」というのが正直なところです。
2020.12.14追記
使い分け方はまちまちだと思いますが、
- プリミティブな値をリアクティブにするならば
ref
- オブジェクトをリアクティブにするならば
reactive
くらいの認識でも良いかと思います。
詳説などは後日なんらかの形で記事にできれば。
Vueにおいては、Single File Componentの記述が主流だったかと思うので、そもそもそこがガラッと変わる変更に近いと思っています。
composition-apiの機能が導入されることによって、状態管理をするロジックの切り出し方についても、これまでの概念とは違うものになってくるかと思います。
「ref
を使わないような設計をしました」
「reactive
とref
をこういう風にうまく使い分けました」
「そもそも状態管理とか(ry」
みたいな議論で今後さらに盛り上がっていくのかな...と楽しみであるとともに、その議論についていくためにも。composition-apiの理解はしっかりしておけると良いと思います。
結論・まとめ
最初の結論のところに書いたのと同じです!!
-
ref
は、プリミティブな(Objectでない)値をリアクティブにする -
reactive
はObjectの値をリアクティブにする -
reactive
に含まれる一部のプロパティの値をリアクティブにしたい場合、toRefs()
を使用する -
ref
でもreactive
でも型推論ちゃんと効くよ - 使い分けのベストプラクティスはまだ存在していない(ケースバイケースとしか)
最後に
冒頭でも申し上げました通り、内容についておかしい点等ございましたら、お手柔らかにご指摘頂けますと幸いです。
そして今年も皆さんお疲れ様でした!
参考資料
いずれも良記事でとても参考になりましたmm