Vue 3 Composition API 完全ガイド:リアクティビティを使いこなす

Vue 3 Composition APIの実践ガイドです。ref、reactive、computed、watch、コンポーザブルを学び、高性能なVueアプリケーションを構築しましょう。

リアクティブなコードブロックが相互接続されたVue 3 Composition APIのイラスト

Vue 3のComposition APIは、Vueコンポーネントの設計方法に大きな変革をもたらしました。オプション単位ではなく機能単位でコードを整理できるため、ロジックの再利用や複雑なアプリケーションの保守が格段に容易になります。

前提知識

このガイドはVue.jsの基礎知識があることを前提としています。各サンプルではVue 3.2で導入された<script setup>構文を使用しています。現在はこの書き方が推奨されています。

refとreactiveによるリアクティビティの理解

リアクティビティはVue 3の中核をなす仕組みです。リアクティブなデータを生成するプリミティブは主に2つあります。プリミティブ値にはref、複雑なオブジェクトにはreactiveを使います。

ref関数は値をラップするリアクティブな参照を生成します。スクリプト内で値にアクセス・変更するには.valueプロパティが必要です。テンプレートではVueが自動的にアンラップするため、.valueは不要です。

Counter.vuejavascript
<script setup>
import { ref } from 'vue'

// Create a ref with initial value 0
const count = ref(0)

// Function that increments the counter
// Note: .value is required in the script
const increment = () => {
  count.value++
}

// Reset function
const reset = () => {
  count.value = 0
}
</script>

<template>
  <!-- In the template, no .value needed -->
  <div class="counter">
    <p>Counter: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="reset">Reset</button>
  </div>
</template>

関連するプロパティが複数あるオブジェクトには、reactiveがより自然な構文を提供します。.valueは不要です。

UserProfile.vuejavascript
<script setup>
import { reactive } from 'vue'

// reactive for complex objects
// All properties are automatically reactive
const user = reactive({
  name: 'Jane Smith',
  email: 'jane@example.com',
  preferences: {
    theme: 'dark',
    notifications: true
  }
})

// Direct property modification (no .value)
const updateTheme = (newTheme) => {
  user.preferences.theme = newTheme
}

// Warning: do not reassign the entire object
// user = { ... } would break reactivity
</script>

<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
    <p>Theme: {{ user.preferences.theme }}</p>
  </div>
</template>

基本的な使い分けは次のとおりです。プリミティブ値(文字列、数値、真偽値)にはrefを、構造化されたオブジェクトにはreactiveを使用します。

computedによる算出プロパティ

算出プロパティはリアクティブな状態から値を導出します。キャッシュが効くため、依存するデータが変更されたときだけ再計算され、高いパフォーマンスを発揮します。

ProductList.vuejavascript
<script setup>
import { ref, computed } from 'vue'

// Product list
const products = ref([
  { id: 1, name: 'Laptop', price: 999, inStock: true },
  { id: 2, name: 'Mouse', price: 29, inStock: true },
  { id: 3, name: 'Keyboard', price: 79, inStock: false },
  { id: 4, name: 'Monitor', price: 299, inStock: true }
])

// Active filter
const showOnlyInStock = ref(false)

// computed: automatic filtering based on toggle
// Only recalculates if products or showOnlyInStock changes
const filteredProducts = computed(() => {
  if (showOnlyInStock.value) {
    return products.value.filter(p => p.inStock)
  }
  return products.value
})

// computed: total calculation with caching
const totalValue = computed(() => {
  return filteredProducts.value.reduce((sum, p) => sum + p.price, 0)
})

// computed: number of displayed items
const productCount = computed(() => filteredProducts.value.length)
</script>

<template>
  <div>
    <label>
      <input type="checkbox" v-model="showOnlyInStock" />
      Show only in-stock products
    </label>

    <p>{{ productCount }} products - Total: ${{ totalValue }}</p>

    <ul>
      <li v-for="product in filteredProducts" :key="product.id">
        {{ product.name }} - ${{ product.price }}
      </li>
    </ul>
  </div>
</template>

算出プロパティはgetterとsetterを組み合わせることで書き込み可能にもなります。双方向の変換が必要な場面で役立ちます。

FullName.vuejavascript
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// computed with getter AND setter
const fullName = computed({
  // Read: combine first and last name
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // Write: split the string into first/last name
  set(newValue) {
    const parts = newValue.split(' ')
    firstName.value = parts[0] || ''
    lastName.value = parts.slice(1).join(' ') || ''
  }
})
</script>

<template>
  <!-- Modifying fullName updates firstName and lastName -->
  <input v-model="fullName" placeholder="Full name" />
  <p>First name: {{ firstName }}</p>
  <p>Last name: {{ lastName }}</p>
</template>

watchとwatchEffectによるウォッチャー

ウォッチャーはデータの変更に応じて副作用を実行します。Vue 3には2つのアプローチがあります。精密に制御できるwatchと、依存関係を自動追跡するwatchEffectです。

watchとwatchEffectの使い分け

watchは依存関係を明示的に指定でき、変更前後の値を取得できます。watchEffectは、使用しているすべてのリアクティブな依存関係で自動的に実行されるため、シンプルに書けます。

SearchComponent.vuejavascript
<script setup>
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)

// watch: observes a specific source
// Provides oldValue and newValue, allows debouncing
watch(searchQuery, async (newQuery, oldQuery) => {
  console.log(`Search changed from "${oldQuery}" to "${newQuery}"`)

  // Don't search if less than 3 characters
  if (newQuery.length < 3) {
    searchResults.value = []
    return
  }

  isLoading.value = true

  try {
    // Simulated API call
    const response = await fetch(`/api/search?q=${newQuery}`)
    searchResults.value = await response.json()
  } finally {
    isLoading.value = false
  }
}, {
  // Watcher options
  debounce: 300, // Wait 300ms after last keystroke
  immediate: false // Don't execute immediately
})

// watchEffect: automatic dependency tracking
// Runs immediately and on every change
watchEffect(() => {
  // All refs used here are automatically tracked
  console.log(`Current state: ${searchResults.value.length} results`)
  console.log(`Loading: ${isLoading.value}`)
})
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="Search..." />
    <p v-if="isLoading">Loading...</p>
    <ul v-else>
      <li v-for="result in searchResults" :key="result.id">
        {{ result.title }}
      </li>
    </ul>
  </div>
</template>

ネストされたオブジェクトや配列を監視するには、watchdeepオプションを指定する必要があります。

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

const settings = reactive({
  display: {
    theme: 'light',
    fontSize: 14
  },
  notifications: {
    email: true,
    push: false
  }
})

// deep: true to observe nested changes
watch(
  () => settings.display,
  (newDisplay) => {
    console.log('Display settings changed:', newDisplay)
    // Save to localStorage for example
    localStorage.setItem('display', JSON.stringify(newDisplay))
  },
  { deep: true }
)
</script>

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

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

再利用可能なコンポーザブルの作成

コンポーザブルとは、再利用可能なリアクティブロジックをカプセル化する関数です。コンポーネント間でコードを共有するうえで、Composition APIの最大の強みの一つです。

composables/useFetch.jsjavascript
import { ref, watchEffect, toValue } from 'vue'

// Composable for HTTP requests
// Automatically handles loading, errors, and refetching
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const isLoading = ref(false)

  // Reusable fetch function
  async function fetchData() {
    isLoading.value = true
    error.value = null

    try {
      // toValue() allows accepting a ref or a value
      const response = await fetch(toValue(url))

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }

      data.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      isLoading.value = false
    }
  }

  // watchEffect for automatic refetch if url is a ref
  watchEffect(() => {
    fetchData()
  })

  // Expose state and methods
  return {
    data,
    error,
    isLoading,
    refetch: fetchData
  }
}

このコンポーザブルを使えば、任意のコンポーネントで宣言的にデータ取得を実装できます。

UserList.vuejavascript
<script setup>
import { useFetch } from '@/composables/useFetch'

// Simple composable usage
const { data: users, isLoading, error, refetch } = useFetch('/api/users')
</script>

<template>
  <div>
    <button @click="refetch" :disabled="isLoading">
      Refresh
    </button>

    <p v-if="isLoading">Loading users...</p>
    <p v-else-if="error" class="error">Error: {{ error }}</p>

    <ul v-else-if="users">
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

次は、バリデーション付きのフォーム状態を管理するコンポーザブルの例です。

composables/useForm.jsjavascript
import { reactive, computed } from 'vue'

// Form management composable
export function useForm(initialValues, validationRules) {
  // Form state
  const form = reactive({
    values: { ...initialValues },
    errors: {},
    touched: {}
  })

  // Validate a specific field
  const validateField = (field) => {
    const rules = validationRules[field]
    if (!rules) return true

    for (const rule of rules) {
      const result = rule(form.values[field])
      if (result !== true) {
        form.errors[field] = result
        return false
      }
    }

    form.errors[field] = null
    return true
  }

  // Validate entire form
  const validate = () => {
    let isValid = true
    for (const field in validationRules) {
      if (!validateField(field)) {
        isValid = false
      }
    }
    return isValid
  }

  // computed: form is valid if no errors
  const isValid = computed(() => {
    return Object.values(form.errors).every(e => !e)
  })

  // Mark a field as touched (to display errors)
  const touch = (field) => {
    form.touched[field] = true
    validateField(field)
  }

  // Reset the form
  const reset = () => {
    form.values = { ...initialValues }
    form.errors = {}
    form.touched = {}
  }

  return {
    form,
    isValid,
    validate,
    validateField,
    touch,
    reset
  }
}

ライフサイクルフックの管理

Composition APIでは、ライフサイクルフックをsetup内で呼び出す関数として提供しています。コンポーネントのライフサイクルの特定のタイミングでコードを実行できます。

クリーンアップの重要性

メモリリークを防ぐため、onUnmountedでタイマー、イベントリスナー、サブスクリプションなどの副作用を必ずクリーンアップしてください。

LifecycleDemo.vuejavascript
<script setup>
import {
  ref,
  onMounted,
  onUnmounted,
  onBeforeUpdate,
  onUpdated
} from 'vue'

const windowWidth = ref(window.innerWidth)
const updateCount = ref(0)

// Handler for resize
const handleResize = () => {
  windowWidth.value = window.innerWidth
}

// onMounted: component inserted into DOM
// Ideal for initial API calls and event listeners
onMounted(() => {
  console.log('Component mounted')
  window.addEventListener('resize', handleResize)

  // Example: initialize a third-party library
  // chart = new Chart(chartRef.value, config)
})

// onUnmounted: component removed from DOM
// CRITICAL: clean up all side effects
onUnmounted(() => {
  console.log('Component unmounted')
  window.removeEventListener('resize', handleResize)

  // Clean up subscriptions, timers, etc.
  // chart?.destroy()
})

// onBeforeUpdate: before DOM update
onBeforeUpdate(() => {
  console.log('Before update')
})

// onUpdated: after DOM update
onUpdated(() => {
  updateCount.value++
  console.log(`DOM updated (${updateCount.value} times)`)
})
</script>

<template>
  <div>
    <p>Window width: {{ windowWidth }}px</p>
    <p>DOM updates: {{ updateCount }}</p>
  </div>
</template>

テンプレート参照とDOMアクセス

テンプレート参照を使うと、DOM要素や子コンポーネントのインスタンスに直接アクセスできます。直接操作が必要なケースで引き続き有用です。

InputFocus.vuejavascript
<script setup>
import { ref, onMounted } from 'vue'

// ref for the DOM element
// Same name as the ref attribute in the template
const inputRef = ref(null)

// Function to focus the input
const focusInput = () => {
  // Access to the native DOM element
  inputRef.value?.focus()
}

// Auto-focus on mount
onMounted(() => {
  focusInput()
})

// Expose the method to parent if needed
defineExpose({
  focus: focusInput
})
</script>

<template>
  <div>
    <!-- The ref attribute binds the element to inputRef -->
    <input ref="inputRef" type="text" placeholder="Auto-focused" />
    <button @click="focusInput">Refocus</button>
  </div>
</template>

PropsとEmitsによる親子間コミュニケーション

Composition APIでは、definePropsdefineEmitsを使ってpropsとイベントの宣言を刷新しています。TypeScriptとの統合も優れています。

ChildComponent.vuejavascript
<script setup>
// defineProps: declares expected props
// With default values via withDefaults
const props = withDefaults(defineProps<{
  title: string
  count?: number
  items?: string[]
}>(), {
  count: 0,
  items: () => []
})

// defineEmits: declares emitted events
// Precise payload typing
const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'submit', data: { title: string; items: string[] }): void
}>()

// Function that emits an event
const handleSubmit = () => {
  emit('submit', {
    title: props.title,
    items: props.items
  })
}

const increment = () => {
  emit('update', props.count + 1)
}
</script>

<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="handleSubmit">Submit</button>
  </div>
</template>
ParentComponent.vuejavascript
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const currentCount = ref(0)
const items = ref(['Item 1', 'Item 2'])

// Handler for update event
const onUpdate = (newValue) => {
  currentCount.value = newValue
}

// Handler for submit event
const onSubmit = (data) => {
  console.log('Data submitted:', data)
}
</script>

<template>
  <ChildComponent
    title="My Component"
    :count="currentCount"
    :items="items"
    @update="onUpdate"
    @submit="onSubmit"
  />
</template>

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

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

Provideとinjectによる依存性注入

プロップスのバケツリレー(prop drilling)を避けて離れたコンポーネント間でデータを共有するために、Vue 3はprovideinjectを提供しています。

App.vue (or an ancestor component)javascript
<script setup>
import { provide, ref, readonly } from 'vue'

// Global theme state
const theme = ref('light')

// Function to toggle theme
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// provide: makes available to descendants
// readonly prevents direct modification by children
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>

<template>
  <div :class="theme">
    <slot />
  </div>
</template>
DeepNestedComponent.vue (anywhere in the tree)javascript
<script setup>
import { inject } from 'vue'

// inject: retrieves value provided by an ancestor
// Default value if not found
const theme = inject('theme', 'light')
const toggleTheme = inject('toggleTheme', () => {})
</script>

<template>
  <div>
    <p>Current theme: {{ theme }}</p>
    <button @click="toggleTheme">
      Switch to {{ theme === 'light' ? 'dark' : 'light' }}
    </button>
  </div>
</template>

まとめ

Vue 3のComposition APIは、Vueアプリケーションのコードを整理するための強力かつ柔軟なアプローチを提供します。押さえておくべきポイントは以下のとおりです。

  • ref はプリミティブ値に、reactive は複雑なオブジェクトに使用する
  • computed で自動キャッシュ付きの派生値を定義する
  • watchwatchEffect でリアクティブな副作用を管理する
  • コンポーザブル でコンポーネント間のロジックを抽出・再利用する
  • 関数型ライフサイクルフックonMountedonUnmounted など)を活用する
  • provide/inject でプロップスのバケツリレーなしに依存性注入を行う

このアプローチにより、保守性とテスト容易性に優れたアプリケーションを構築しながら、TypeScriptとの優れた統合も実現できます。次のステップとして、非同期コンポーザブルやPiniaを使ったグローバルステート管理などの高度なパターンを探求してみてください。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#vue 3
#composition api
#javascript
#frontend
#reactivity

共有

関連記事