Hanatare's PaPa

Make life a little richer.

Virtual Space of Hanatare's PaPa

人生をほんの少しだけ充実させる

【Vue.js 3】refとreactiveのどちらを使うか問題について考える

Vue.jsのComposition APIを使い始めて、早速「refとreactive、結局どちらを使えば良いのか?」という疑問を持ちました。refもreactiveもVue.jsのリアクティビティを実現するにために使う関数なのですが、動作・使用方法が似ているので、適切に使いこなすために、その仕組みを深く理解し使いこなせるようになるために、記事にしようと思いました。

記事のポイント
  • Vue.jsにおけるリアクティビティを理解する
  • ref()関数を理解する
  • reactive関数を理解する
  • refとreactiveの使い分け

Vue.jsにおけるリアクティビティとは?

Vue.js最も特徴的な機能の一つが「リアクティビティ」です。これは、データが変更された際に、それに関連するUI(ユーザーインターフェース)が自動的に更新される仕組みを指します。開発者が手動でDOM(Document Object Model)を操作する手間を省き、宣言的なプログラミングによって実現が可能です。

このリアクティビティは、Vue.jsのフレームワークの中核をなす機能であり、公式ドキュメントでも「リアクティビティーの探求」という専用ページが設けられているほど、その重要性が強調されています。

ja.vuejs.org

JavaScriptのProxyオブジェクト

Vue.jsリアクティビティはJavaScriptのProxyオブジェクトを用いて実現されています。Proxyは、オブジェクトのプロパティへのアクセスや変更といった操作を「傍受」し、その変更を監視する役割を担います。これにより、Vue.jsはどのデータがどのコンポーネントで使われているかを自動的に追跡し、データに変更があった際に、影響を受けるコンポーネントのみを効率的に再レンダリングすることが可能になります 。

Proxyベースのリアクティビティシステムを深く理解することは、今回の記事の目的である、refとreactiveの挙動を理解する上で非常に重要です。Proxyはオブジェクトのプロパティへのアクセス時に機能します。プリミティブな値(文字列、数値、真偽値など)は直接Proxyの対象とはならないことに注意が必要です。

ref() を徹底解説:プリミティブからオブジェクトまで

ref()は、Vue.jsのComposition APIにおいて、主にプリミティブな値(string、number、booleanなど)をリアクティブにするために使用されます 。refがプリミティブ値をリアクティブにする際には、その値をRefオブジェクトという特殊なオブジェクトでラップしています。

以下の記述のように、ref(0)やref('Hello Vue!')のように、初期値を引数に渡して宣言します。

サンプルコード:プリミティブ値のref

<script setup>
import { ref } from 'vue'

const count = ref(0) // 数値のref
const message = ref('Hello Vue!') // 文字列のref

const increment = () => {
  count.value++ //.value を介して値にアクセス・変更
}

console.log(count.value) // 0
increment()
console.log(count.value) // 1
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Message: {{ message }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

refで定義したリアクティブな値にscript内でアクセスする

scriptタグ内で、refで定義されたリアクティブな値にアクセスする際には、必ず.valueプロパティを介する必要があります。上記のサンプルスクリプトの記述で言うと、「count.value」が該当します。.valueの存在は、その変数がリアクティブなRefオブジェクトであることをコード上で明示的に示します 。これによって、コードの可読性を上げ、デバッグの際にもその変数がリアクティブな状態であることをすぐに判別できるため、予期せぬ挙動を未然に防ぐといった効果が得られます。

refはプリミティブな値だけでなく、オブジェクトも扱える

ref()はプリミティブ値だけでなく、オブジェクトや配列、Mapのような非プリミティブ値も引数に取ることができます 。この場合、refは内部的に後述するreactive()を使用して、そのオブジェクトをリアクティブなProxyに変換しています 。つまり、refは単なるプリミティブ用のラッパーではなく、Vueのリアクティビティシステムにおける、エントリーポイントとして扱えます。開発者は、値の型を意識せずに常にrefを利用できるため、後述するreactiveの再割り当て問題のような落とし穴を回避するの役割も果たします。

サンプルコード:オブジェクトをrefでラップする

<script setup>
import { ref } from 'vue'

const user = ref({
  name: 'Hanatare Papa',
  age: 30,
})

const updateName = () => {
  user.value.name = 'Hanatare Mama' // オブジェクトのプロパティにアクセス
}

console.log(user.value.name) // Hanatare Papa
console.log(user.value.name) // Hanatare Mama
</script>

<template>
  <div>
    <p>User Name: {{ user.name }}</p>
    <p>User Age: {{ user.age }}</p>
    <button @click="updateName">Update Name</button>
  </div>
</template>

refで定義したリアクティブな値にtemplate内でアクセスする

Vueのtemplate内でrefの値をアクセスする場合、Vueは変数の値がrefオブジェクトかどうかを自動的判別し、.valueを省略してくれるため、開発者はテンプレート内で.valueを記述する必要がありません。例えば、のように、直接countと記述するだけで値にアクセスできます 。

オブジェクト内の値にrefオブジェクトがある場合は注意が必要

template内でrefオブジェクトの値にアクセスする場合、.valueは省略できると記載しましたが例外があります。それはオブジェクトの中にrefオブジェクトが存在する場合です。以下のように、userオブジェクトの中にref(30)としたオブジェクト内にrefオブジェ句がある場合は、templaete内でも.valueをつけないとアクセスができません。これは、Veu.jsは変数の最初の記述だけを見て、そのオブジェクトがref関数かどうかを判断しているため、userが普通のオブジェクトと判断してしまい、userオブジェクト内にあるrefオブジェクトが無視されてしまいます。

サンプルソース:オブジェクト内の値にrefオブジェクトがある場合

<script setup>
import { ref } from 'vue'

const user = {
  name: 'Hanatare Papa',
  age: ref(30),
}
console.log(user.age.value) //.valueを付ける
</script>

<template>
  <div>
    <p>User Name: {{ user.name }}</p>
    <p>User Age: {{ user.age.value }}</p>  <!-- .valueを付ける-->
    <button @click="updateName">Update Name</button>
  </div>
</template>

reactive() を徹底解説:オブジェクト指向のリアクティブ

reactive()は、オブジェクトや配列、Map、Setといった非プリミティブ値をリアクティブにするために設計されています。複数の関連するプロパティを持つ複雑な状態を一つのまとまりとして扱いたい場合に特に適しています。例えば、ユーザー情報やフォームの入力値など、複数のデータ項目をグループ化して管理する際に活用されます。

サンプルコード:reactiveオブジェクトの宣言とアクセス

<script setup>
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  message: 'Hello Reactive!',
})

const increment = () => {
  state.count++ //.value は不要
}

console.log(state.message) // Hello Reactive!
increment()
console.log(state.count) // 1
</script>

<template>
  <div>
    <p>Count: {{ state.count }}</p>
    <p>Message: {{ state.message }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

reactiveで定義したリアクティブな値にアクセスする(script・template共通)

reactiveで定義されたオブジェクトのプロパティにアクセスする際には、refのように.valueを介する必要がなく、通常のJavaScriptオブジェクトのように直接プロパティにアクセスできます。.valueが不要な点は、コードをより簡潔にし、JavaScriptの通常のオブジェクト操作に近い感覚で扱えるため、直感的に感じられることがあります。

reactiveオブジェクト内にrefオブジェクトがある場合は注意が必要

reactiveオブジェクトの中にrefオブジェクトがある場合、refオブジェクトに対してはvalueを付けるケースと、つけないケースがあるため、注意が必要です。

.valueを付けないケース

こちらが一般的だと思います。reactiveオブジェクトの中に、refオブジェクトが存在する場合、refオブジェクトにアクセスする際、valueをつける必要はありません。

サンプルコード:reactiveオブジェクト中にrefオブジェクトが存在する場合

<script setup>
import { ref, reactive } from 'vue'

const state = reactive({
  count: 0,
  message: ref('Hello Reactive!'), // reactiveオブジェクトの中にrefオブジェクトが存在する
})

const increment = () => {
  state.count++ //.value は不要
}
console.log(state.message) // .valueは不要
</script>

<template>
  <div>
    <p>Count: {{ state.count }}</p>
    <p>Message: {{ state.message }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

.valueを付けるケース

配列の要素にrefオブジェクトがある場合は、.valueを付ける必要があります。

サンプルコード:reactiveオブジェクトの中に配列があり、その要素にrefオブジェクトがある場合

<script setup>
import { ref, reactive } from 'vue'

const state = reactive([ref(0), ref(1), 2])

console.log(state[0].value + 1) // .valueは不要
console.log(state[2] + 2) //
</script>

<template>
  <div>
    <p>State: {{ state[0].value + 1 }}</p>
    <p>State2: {{ state[2] }}</p>
  </div>
</template>

リアクティビティの喪失

reactive()を使用する上で非常に重要な注意点があります。それは、reactiveで定義されたオブジェクトは、そのオブジェクト自体を新しいオブジェクトで「まるっと」置き換えると、リアクティビティが失われるという点です 。これは、VueのリアクティビティがProxyによって元のオブジェクトの参照を追跡しているためであり、新しいオブジェクトへの参照は追跡されないためです。

サンプルコード:リアクティビティの喪失

<script setup>
import { reactive, watch } from 'vue'

let user = reactive({
  firstName: 'Michael',
  lastName: 'Scott',
})
console.log(user)
// userオブジェクトの変更を監視
watch(
  user,
  (newUser) => {
    console.log('User object changed:', newUser)
  },
  { deep: true },
)
const changeUser = () => {
  user.firstName = 'Hanatare'
  console.log('User Change:', user.firstName)
}

const changeNewUser = () => {
  // 新しいオブジェクトでuserを再割り当て
  user = reactive({
    // ここで新しいProxyが作成される
    firstName: 'Jim',
    lastName: 'Halpert',
  })
  console.log('User reassigned:', user.firstName)
  // この変更は、watchで追跡されません。
  // なぜなら、watchは元のuserオブジェクトの参照を監視しているためです。
}
</script>

<template>
  <div>
    <p>First Name: {{ user.firstName }}</p>
    <p>Last Name: {{ user.lastName }}</p>
    <button @click="changeUser">Change User (Reassign)</button>
    <button @click="changeNewUser">Change NewUser (Reassign)</button>
  </div>
</template>

上記の例では、changeNewUser関数内でuserオブジェクトを新しいオブジェクトで再割り当てしています。このとき、watchは元のuserオブジェクトの参照を監視しているため、新しいオブジェクトへの変更を検知できません 。

「オブジェクトの再割り当て問題」について、開発者はreactiveの利便性の裏に潜むリスクを理解し、より慎重になる必要があります。オブジェクト全体を置き換える必要がある場合は、refを使用するか、Object.assign()やスプレッド構文などを用いて既存のオブジェクトのプロパティを更新する方法を検討する必要があります。

さらに、reactiveオブジェクトのプロパティを直接取り出して別の関数に渡したり、分割代入したりすると、そのプロパティがリアクティブ性を失うことがあります 。これは、reactiveオブジェクトから取り出された値が、元のProxyオブジェクトの参照を失い、非リアクティブな値として扱われるためです。この「参照透過性」の限界は、特にコンポーザブル関数などで状態を共有する際に顕在化し、予期せぬバグにつながる可能性があります。この問題に対する解決策としてtoRefs()が提供されています。

toRefs()関数の理解が、reactive利用の真価につながる

toRefs()は、reactiveオブジェクトの各プロパティを個別のrefに変換し、リアクティビティを維持したまま利用できるようにします 。

サンプロコード:toRefs()の活用

<script setup>
import { reactive, toRefs } from 'vue'

const userProfile = reactive({
  name: 'John Doe',
  email: 'john.doe@example.com',
  age: 30,
})

// userProfileのプロパティを個別のrefに変換
// これにより、分割代入してもリアクティビティが維持される
const { name, email } = toRefs(userProfile)

const updateProfile = () => {
  name.value = 'Jane Smith' // nameはrefなので.valueが必要
  userProfile.email = 'jane.smith@example.com' // userProfile経由でも更新可能
}
</script>

<template>
  <div>
    <p>Name: {{ name }}</p>
    <p>Email: {{ email }}</p>
    <p>Age: {{ userProfile.age }}</p>
    <button @click="updateProfile">Update Profile</button>
  </div>
</template>

上記の例では、userProfileというreactiveオブジェクトのnameとemailプロパティをtoRefsを使って個別のrefに変換しています。これにより、nameとemailを分割代入しても、元のuserProfileオブジェクトとのリアクティブな接続が維持され、テンプレートや他のリアクティブな計算で変更が適切に反映されます。

refとreactiveの結局どちらを使うべき?

Vue.jsコミュニティの専門家や公式に近い見解として、現在では「迷ったらrefを使うべき」という推奨が主流となっています 。

まず、refはその汎用性の高さが挙げられます。プリミティブ、オブジェクト、配列など、あらゆる型の値を一貫した方法(.valueアクセス)で扱えるため、コードの統一性を保ち、学習コストを低減する利点があります 。特に、reactiveの大きな落とし穴である「オブジェクト再割り当てによるリアクティビティ喪失」をrefが回避できる点は、予期せぬバグを防ぐ上で非常に重要です 。refは、プリミティブでもオブジェクトでも一貫して.valueを介してアクセスするという特性を持ちます。この一貫性は、開発者が値の型によって異なるアクセス方法を覚える必要がなくなり、コードの予測可能性を高めます。結果として、認知負荷が軽減され、開発体験が向上するという波及効果があります。特に大規模なプロジェクトや複数の開発者が関わる場合、このような一貫性はバグの削減とメンテナンス性の向上に大きく貢献します。

reactiveが特に適している具体的なユースケース

では、refを全てにおいて使うと良いとなりそうなのですが、reactiveが有用なケースもあります。

関連する状態のグループ化

複数の関連するプロパティを持つ複雑な状態を一つのオブジェクトとしてまとめたい場合にreactiveが適しています 。これにより、コードの構造がより論理的になり、可読性が向上します。例えば、フォームの入力データやユーザープロファイルなど、論理的にまとまったデータを扱う際に有効です。

既存のリアクティブオブジェクトの利用

propsのように、Vue.jsフレームワークによって既にreactiveとして提供されているオブジェクトを扱う場合、そのままreactiveとして利用するのが自然です 。これらのオブジェクトは、Composition APIのsetup関数や「script setup」内で直接reactiveとして扱われます。

特定のJavaScriptビルトインオブジェクトをリアクティブにしたい場合

MapやSetなど、特定のJavaScriptビルトインオブジェクトをリアクティブにしたい場合、reactiveはこれらの型を直接リアクティブにできるため、特定の高度なユースケースで役立つことがあります 。

まとめ

本記事では、Vue.jsのComposition APIにおけるrefとreactiveの違い、内部的な仕組み、そして適切な使い分けについて、掘り下げて解説しました。 その上で、refとreactiveどちらを選ぶべきかについては、ある程度、今は決着がついているように思います。基本的にはrefを利用し、その上でユースケースに応じてreatvieを使うというのが今の一般的な流れのようです。今回の記事を書くにあたり得た知識を活かし、それぞれの状況に合わせた最適な選択ができるようになることで、Vue.jsでの開発がよりスムーズで、且つバグを少なくした開発が実現できるようになれればと思います。 今回の記事がVue.jsを利用される方の参考になれば幸いです。