Vue 3 Reactivity Transform 2026:$ref、$computed とインタビュー対策

Vue 3のReactivity Transform($ref、$computed)について深く解説します。2026年の最新動向、廃止の背景、現在の代替手段、そして技術面接で頻出する質問を詳しく取り上げます。

Vue 3 Reactivity Transform: $ref, $computed and Interview Questions

Vue 3のReactivity Transformは、Composition APIの開発体験を改善するために導入された実験的機能です。$ref$computed$shallowRefなどのマクロを使用することで、.valueの記述を省略できる構文糖衣を提供していました。しかし、Vue 3.3でこの機能は廃止され、現在は推奨されていません。技術面接では、この機能の歴史、廃止理由、そして代替手段についての理解が問われます。

Reactivity Transform の現状

Reactivity TransformはVue 3.3で廃止されましたが、@vue-macros/reactivity-transformパッケージを使用すれば引き続き利用可能です。ただし、新規プロジェクトでの使用は推奨されません。

Reactivity Transformの基本概念と歴史

Reactivity Transformは、Vue 3.2で実験的機能として導入されました。主な目的は、refを使用する際に毎回.valueを記述する煩雑さを解消することでした。コンパイル時にマクロを通常のVueコードに変換する仕組みです。

typescript
// Reactivity Transform を使用した場合(廃止された構文)
<script setup>
let count = $ref(0)
let doubled = $computed(() => count * 2)

function increment() {
  count++
}
</script>

<template>
  <button @click="increment">Count: {{ count }}</button>
  <p>Doubled: {{ doubled }}</p>
</template>

このコードはコンパイル時に以下のように変換されます。

typescript
// コンパイル後の実際のコード
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">Count: {{ count }}</button>
  <p>Doubled: {{ doubled }}</p>
</template>

廃止された理由と技術的背景

VueチームがReactivity Transformを廃止した理由は複数あります。技術面接ではこれらの理由について説明を求められることがあります。

1. 明示性と予測可能性の欠如

Reactivity Transformは、変数がリアクティブかどうかを見分けることを困難にしました。通常の変数と$refで宣言された変数は視覚的に区別がつきにくく、コードレビューやデバッグ時に混乱を招きました。

typescript
// 問題: どちらがリアクティブか一目で分からない
let count = $ref(0)     // リアクティブ
let total = 0           // 非リアクティブ

function update() {
  count++  // UIが更新される
  total++  // UIが更新されない
}

2. ツールサポートの複雑さ

IDEやlinterがReactivity Transformを正しく理解するには、特別な設定やプラグインが必要でした。TypeScriptの型推論との統合も完全ではありませんでした。

typescript
// TypeScriptとの統合問題
let items = $ref<string[]>([])

// 以下のような操作で型エラーが発生することがあった
items.push('new item')  // 型推論が正しく機能しないケース

3. リアクティビティの喪失リスク

関数に渡す際や分割代入時にリアクティビティが失われるケースがありました。これは$$マクロで回避できましたが、学習コストが増大しました。

typescript
// リアクティビティ喪失の例
let count = $ref(0)

function useCounter(c) {
  // cはここで既にプリミティブ値
  console.log(c)  // 0
}

useCounter(count)  // リアクティビティが失われる

// $$マクロを使用した回避策
useCounter($$(count))  // refとして渡される

現在推奨される代替手段

Vue 3.4以降では、Reactivity Transformの代わりに以下のアプローチが推奨されています。

ref()の標準的な使用

.valueの記述は冗長に感じるかもしれませんが、コードの明示性と予測可能性が向上します。

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

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">Count: {{ count }}</button>
  <p>Doubled: {{ doubled }}</p>
</template>

Composable関数でのロジック抽出

複雑なリアクティブロジックはComposable関数に抽出することで、コードの再利用性と保守性が向上します。

composables/useCounter.tstypescript
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)
  const isEven = computed(() => count.value % 2 === 0)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    isEven,
    increment,
    decrement,
    reset
  }
}

reactive()とtoRefs()の組み合わせ

複数の関連する状態を管理する場合、reactive()toRefs()の組み合わせが効果的です。

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

const state = reactive({
  firstName: '',
  lastName: '',
  email: ''
})

const { firstName, lastName, email } = toRefs(state)

const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

技術面接での頻出質問

Q1: refとreactiveの違いは何ですか?

typescript
import { ref, reactive, isRef, isReactive } from 'vue'

// refはプリミティブ値をラップ
const count = ref(0)
console.log(isRef(count))  // true
console.log(count.value)   // 0

// reactiveはオブジェクトをプロキシでラップ
const state = reactive({ count: 0 })
console.log(isReactive(state))  // true
console.log(state.count)        // 0(.valueは不要)

// reactiveの注意点
const { count: destructuredCount } = state  // リアクティビティ喪失

Q2: shallowRefとrefの違いは何ですか?

typescript
import { ref, shallowRef, triggerRef } from 'vue'

// refは深いリアクティビティを提供
const deepRef = ref({ nested: { value: 1 } })
deepRef.value.nested.value = 2  // 変更が検知される

// shallowRefは浅いリアクティビティのみ
const shallow = shallowRef({ nested: { value: 1 } })
shallow.value.nested.value = 2  // 変更は検知されない
shallow.value = { nested: { value: 2 } }  // 全体の置き換えは検知される

// 手動でトリガーする場合
shallow.value.nested.value = 3
triggerRef(shallow)  // 明示的に更新を通知

Q3: watchとwatchEffectの使い分けは?

typescript
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const results = ref([])

// watchEffect: 依存関係を自動追跡
watchEffect(async () => {
  // searchQueryの変更で自動的に再実行
  if (searchQuery.value.length > 2) {
    results.value = await fetchResults(searchQuery.value)
  }
})

// watch: 明示的な依存関係指定
watch(searchQuery, async (newValue, oldValue) => {
  console.log(`Changed from ${oldValue} to ${newValue}`)
  if (newValue.length > 2) {
    results.value = await fetchResults(newValue)
  }
}, { immediate: false, deep: false })

// 複数のソースを監視
watch(
  [searchQuery, () => state.filter],
  ([newQuery, newFilter], [oldQuery, oldFilter]) => {
    // 両方の変更に反応
  }
)

Vue.js / Nuxt.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Q4: customRefはどのような場面で使用しますか?

typescript
import { customRef } from 'vue'

// デバウンス付きのref実装
function useDebouncedRef<T>(value: T, delay = 300) {
  let timeout: ReturnType<typeof setTimeout>
  
  return customRef<T>((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue: T) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

// 使用例
const searchInput = useDebouncedRef('', 500)

Q5: toRefとtoRefsの違いを説明してください

typescript
import { reactive, toRef, toRefs } from 'vue'

const state = reactive({
  firstName: 'John',
  lastName: 'Doe',
  age: 30
})

// toRef: 単一のプロパティをrefに変換
const ageRef = toRef(state, 'age')
ageRef.value++  // state.ageも31になる

// toRefs: 全プロパティをrefsに変換
const { firstName, lastName } = toRefs(state)
firstName.value = 'Jane'  // state.firstNameも'Jane'になる

// Composableでの活用
function useUserState() {
  const state = reactive({ name: '', email: '' })
  return toRefs(state)  // 分割代入してもリアクティビティを維持
}

Vue 3.5以降のリアクティビティ改善

Vue 3.5では、Reactivity Transformに代わる新しい改善が導入されています。

Reactive Props Destructure

Vue 3.5では、definePropsから分割代入しても、リアクティビティが維持されるようになりました。

typescript
<script setup>
// Vue 3.5以降: 分割代入してもリアクティブ
const { title, count = 0 } = defineProps<{
  title: string
  count?: number
}>()

// watchで監視可能
watch(() => count, (newVal) => {
  console.log('count changed:', newVal)
})
</script>

<template>
  <h1>{{ title }}</h1>
  <p>Count: {{ count }}</p>
</template>

useTemplateRef

Vue 3.5では、テンプレート参照を取得するためのuseTemplateRef関数が追加されました。

typescript
<script setup>
import { useTemplateRef, onMounted } from 'vue'

const inputRef = useTemplateRef('input')

onMounted(() => {
  inputRef.value?.focus()
})
</script>

<template>
  <input ref="input" type="text" />
</template>

パフォーマンス最適化のベストプラクティス

技術面接では、リアクティビティシステムのパフォーマンス最適化についても質問されることがあります。

typescript
import { shallowRef, shallowReactive, markRaw, computed } from 'vue'

// 大きなリストにはshallowRefを使用
const largeList = shallowRef<Item[]>([])

// 変更不要なオブジェクトはmarkRawでラップ
const config = markRaw({
  apiUrl: 'https://api.example.com',
  timeout: 5000
})

// computedのキャッシュを活用
const expensiveComputed = computed(() => {
  // 依存する値が変わらない限り再計算されない
  return largeList.value.filter(item => item.active).length
})

// 不必要なリアクティビティを避ける
const staticData = {
  labels: ['A', 'B', 'C'],  // 変更されないデータ
  constants: Object.freeze({ MAX: 100 })
}

まとめ

Vue 3のReactivity Transformは、開発体験の向上を目指した野心的な試みでしたが、明示性とツールサポートの問題から廃止されました。現在のVue開発では、標準的なref()computed()を使用し、複雑なロジックはComposable関数に抽出することが推奨されています。

技術面接では、Reactivity Transformの歴史と廃止理由を理解していることに加え、refreactivewatchcomputedなどのCore Reactivity APIを適切に使い分けられることが重要です。Vue 3.5で導入されたReactive Props DestructureやuseTemplateRefなどの新機能も把握しておくと、より高い評価を得られます。

今日のチャレンジ

Vue.js / Nuxt.js のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月9日 更新

タグ

#vue
#reactivity
#interview
#composition-api

共有

関連記事