💉

Vue3のProvide / Injectを完全理解!依存性注入の基本から実践まで

に公開1

Vue3のProvide / Injectを完全理解!依存性注入の基本から実践まで

Vue3のProvide / Injectは、コンポーネント間でデータや機能を効率的に共有するための強力な機能です。この記事では、Provide / Injectの基本概念から実践的な応用パターンまで、段階的に詳しく解説していきます。

Provide / Injectとは

Provide / Injectは、Vue3の依存性注入(Dependency Injection)パターンを実装するための機能です。親コンポーネントでprovideを使ってデータや機能を提供し、子孫コンポーネントでinjectを使ってそれらを受け取ることができます。

基本的なProvide / Injectの例

<!-- ParentComponent.vue -->
<template>
  <div>
    <ChildComponent />
  </div>
</template>

<script setup>
import { provide } from 'vue'
import ChildComponent from './ChildComponent.vue'

// データを提供
provide('message', 'Hello from parent!')
provide('count', 0)
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
  </div>
</template>

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

// データを受け取り
const message = inject('message')
const count = inject('count')
</script>

基本的な使い方

1. 文字列や数値の提供

<!-- ParentComponent.vue -->
<script setup>
import { provide } from 'vue'

provide('appName', 'My Vue App')
provide('version', '1.0.0')
provide('isProduction', true)
</script>
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'

const appName = inject('appName')
const version = inject('version')
const isProduction = inject('isProduction')

console.log(`${appName} v${version}`) // "My Vue App v1.0.0"
</script>

2. オブジェクトの提供

<!-- ParentComponent.vue -->
<script setup>
import { provide, reactive } from 'vue'

const user = reactive({
  name: '田中太郎',
  email: 'tanaka@example.com',
  role: 'admin'
})

provide('user', user)
</script>
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'

const user = inject('user')
console.log(user.name) // "田中太郎"
</script>

デフォルト値の設定

injectの第二引数でデフォルト値を設定できます。

<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'

// デフォルト値を設定
const theme = inject('theme', 'light')
const language = inject('language', 'ja')
const apiUrl = inject('apiUrl', 'https://api.example.com')
</script>

関数の提供

関数もProvide / Injectで共有できます。

<!-- ParentComponent.vue -->
<script setup>
import { provide } from 'vue'

const showNotification = (message) => {
  alert(message)
}

const logError = (error) => {
  console.error('Error:', error)
}

provide('showNotification', showNotification)
provide('logError', logError)
</script>
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'

const showNotification = inject('showNotification')
const logError = inject('logError')

// 関数を使用
const handleClick = () => {
  showNotification('ボタンがクリックされました!')
}
</script>

リアクティブなデータの提供

refを使ったリアクティブなデータ

<!-- ParentComponent.vue -->
<script setup>
import { provide, ref } from 'vue'

const count = ref(0)
const increment = () => count.value++

provide('count', count)
provide('increment', increment)
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

const count = inject('count')
const increment = inject('increment')
</script>

reactiveを使ったリアクティブなオブジェクト

<!-- ParentComponent.vue -->
<script setup>
import { provide, reactive } from 'vue'

const state = reactive({
  todos: [],
  addTodo: (text) => {
    state.todos.push({ id: Date.now(), text, completed: false })
  },
  toggleTodo: (id) => {
    const todo = state.todos.find(t => t.id === id)
    if (todo) todo.completed = !todo.completed
  }
})

provide('todoState', state)
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <ul>
      <li v-for="todo in todoState.todos" :key="todo.id">
        <input 
          type="checkbox" 
          :checked="todo.completed"
          @change="todoState.toggleTodo(todo.id)"
        >
        {{ todo.text }}
      </li>
    </ul>
    <button @click="addTodo">Add Todo</button>
  </div>
</template>

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

const todoState = inject('todoState')
const addTodo = () => todoState.addTodo('New Todo')
</script>

実践的な応用パターン

1. テーマ管理システム

<!-- ThemeProvider.vue -->
<template>
  <div :class="theme">
    <slot />
  </div>
</template>

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

const currentTheme = ref('light')

const theme = computed(() => ({
  light: 'theme-light',
  dark: 'theme-dark'
}[currentTheme.value]))

const toggleTheme = () => {
  currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'
}

provide('theme', {
  currentTheme,
  theme,
  toggleTheme
})
</script>
<!-- ThemedButton.vue -->
<template>
  <button :class="themeClass" @click="handleClick">
    <slot />
  </button>
</template>

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

const theme = inject('theme')
const themeClass = computed(() => `btn-${theme.currentTheme.value}`)

const handleClick = () => {
  theme.toggleTheme()
}
</script>

2. APIクライアントの共有

<!-- ApiProvider.vue -->
<script setup>
import { provide } from 'vue'

class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL
  }

  async get(endpoint) {
    const response = await fetch(`${this.baseURL}${endpoint}`)
    return response.json()
  }

  async post(endpoint, data) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    return response.json()
  }
}

const apiClient = new ApiClient('https://api.example.com')
provide('apiClient', apiClient)
</script>
<!-- UserList.vue -->
<template>
  <div>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

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

const apiClient = inject('apiClient')
const users = ref([])

onMounted(async () => {
  users.value = await apiClient.get('/users')
})
</script>

3. モーダル管理システム

<!-- ModalProvider.vue -->
<template>
  <div>
    <slot />
    <div v-if="isOpen" class="modal-overlay" @click="close">
      <div class="modal-content" @click.stop>
        <component :is="modalComponent" v-bind="modalProps" />
      </div>
    </div>
  </div>
</template>

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

const isOpen = ref(false)
const modalComponent = ref(null)
const modalProps = ref({})

const openModal = (component, props = {}) => {
  modalComponent.value = component
  modalProps.value = props
  isOpen.value = true
}

const closeModal = () => {
  isOpen.value = false
  modalComponent.value = null
  modalProps.value = {}
}

provide('modal', {
  openModal,
  closeModal
})
</script>
<!-- UserProfile.vue -->
<template>
  <div>
    <h2>{{ user.name }}</h2>
    <button @click="openEditModal">編集</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'
import EditUserModal from './EditUserModal.vue'

const modal = inject('modal')
const user = { name: '田中太郎', email: 'tanaka@example.com' }

const openEditModal = () => {
  modal.openModal(EditUserModal, { user })
}
</script>

型安全性の向上

TypeScriptを使用する場合、型安全性を向上させることができます。

// types.ts
export interface User {
  id: number
  name: string
  email: string
}

export interface ApiClient {
  get<T>(endpoint: string): Promise<T>
  post<T>(endpoint: string, data: any): Promise<T>
}
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { provide } from 'vue'
import type { User, ApiClient } from './types'

const user: User = {
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com'
}

const apiClient: ApiClient = {
  async get<T>(endpoint: string): Promise<T> {
    // 実装
  },
  async post<T>(endpoint: string, data: any): Promise<T> {
    // 実装
  }
}

provide<User>('user', user)
provide<ApiClient>('apiClient', apiClient)
</script>
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import type { User, ApiClient } from './types'

const user = inject<User>('user')
const apiClient = inject<ApiClient>('apiClient')

// 型安全に使用可能
if (user) {
  console.log(user.name) // 型チェック済み
}
</script>

ベストプラクティス

1. キーの命名規則

// 良い例:明確で一意なキー名
provide('userService', userService)
provide('themeConfig', themeConfig)
provide('apiClient', apiClient)

// 悪い例:曖昧で重複しやすいキー名
provide('data', data)
provide('config', config)
provide('service', service)

2. シンボルキーの使用

// シンボルキーを使用して名前の衝突を防ぐ
const USER_SERVICE_KEY = Symbol('userService')
const THEME_CONFIG_KEY = Symbol('themeConfig')

provide(USER_SERVICE_KEY, userService)
provide(THEME_CONFIG_KEY, themeConfig)

3. エラーハンドリング

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

const userService = inject('userService')

if (!userService) {
  throw new Error('userService is not provided')
}

// 安全に使用
userService.getUser()
</script>

注意点と制限

1. プロパティの上書き

<!-- 注意:子コンポーネントでprovideした値は親の値を上書きする -->
<script setup>
// 親で提供された値を上書き
provide('message', 'Child message')
</script>

2. リアクティビティの保持

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

const state = reactive({ count: 0 })

// リアクティビティを保持するためにtoRefsを使用
provide('state', toRefs(state))
</script>

まとめ

Vue3のProvide / Injectは、コンポーネント間でのデータ共有を効率的に行うための強力な機能です。主なポイントは以下の通りです:

  • 依存性注入パターン:親から子孫コンポーネントへのデータ提供
  • リアクティブなデータ:refやreactiveと組み合わせてリアクティブな状態管理
  • 型安全性:TypeScriptと組み合わせて型安全な開発
  • 実践的な応用:テーマ管理、APIクライアント、モーダル管理など

適切に使用することで、コンポーネントの再利用性と保守性を大幅に向上させることができます。プロパティのドリリング(props drilling)を避け、クリーンなアーキテクチャを構築するために、Provide / Injectを積極的に活用していきましょう。

GitHubで編集を提案

Discussion

junerjuner

あれ、この場合って InjectionKey<T> にキャストして使った方が良いのでは……?(型指定が不要になるのでは……?

<script setup lang="ts">
import { inject, type InjectionKey } from 'vue'
import type { User, ApiClient } from './types'

const userKey = "user" as unknown as InjectionKey<User>;
const apiClientKey = "apiClient" as unknown as InjectionKey<ApiClient>;

const user = inject(userKey);
const apiClient = inject(apiClientKey);
// ...
</script>