💉
Vue3のProvide / Injectを完全理解!依存性注入の基本から実践まで
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を積極的に活用していきましょう。
Discussion
あれ、この場合って
InjectionKey<T>にキャストして使った方が良いのでは……?(型指定が不要になるのでは……?