provide/inject в Vue.js
Что такое provide/inject?
provide и inject — это пара API для передачи данных от родительского компонента к любому потомку без явной передачи через props на каждом уровне.
Это решает проблему "prop drilling" — когда нужно пробрасывать props через множество компонентов.
Базовое использование
Composition API
<!-- App.vue (родитель) -->
<template>
<ChildComponent />
</template>
<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- GrandchildComponent.vue (потомок) -->
<template>
<div :class="theme">
Тема: {{ theme }}
</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
</script>
Options API
<!-- App.vue -->
<script>
export default {
provide() {
return {
theme: 'dark'
}
}
}
</script>
<!-- GrandchildComponent.vue -->
<script>
export default {
inject: ['theme'],
mounted() {
console.log(this.theme) // 'dark'
}
}
</script>
Реактивность
Передача реактивных данных
<!-- App.vue -->
<template>
<button @click="theme = theme === 'dark' ? 'light' : 'dark'">
Переключить тему
</button>
<ChildComponent />
</template>
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
// Передаём ref, не значение
provide('theme', theme)
</script>
<!-- DeepChild.vue -->
<template>
<div :class="theme">
Текущая тема: {{ theme }}
</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
// theme автоматически обновится при изменении
</script>
Передача функций для изменения
<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
provide('count', count)
provide('increment', increment)
</script>
<!-- DeepChild.vue -->
<template>
<p>Count: {{ count }}</p>
<button @click="increment">Увеличить</button>
</template>
<script setup>
import { inject } from 'vue'
const count = inject('count')
const increment = inject('increment')
</script>
Значения по умолчанию
<script setup>
import { inject } from 'vue'
// Если 'theme' не предоставлен, используется 'light'
const theme = inject('theme', 'light')
// Значение по умолчанию как функция
const user = inject('user', () => ({ name: 'Guest' }))
// Третий параметр — значение вычисляется как фабрика
const config = inject('config', () => {
return { /* сложная логика */ }
}, true)
</script>
Symbol ключи
Использование Symbol предотвращает конфликты имён:
// keys.js
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')
<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
import { ThemeKey, UserKey } from './keys'
const theme = ref('dark')
const user = ref({ name: 'Иван' })
provide(ThemeKey, theme)
provide(UserKey, user)
</script>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
import { ThemeKey, UserKey } from './keys'
const theme = inject(ThemeKey)
const user = inject(UserKey)
</script>
Типизация в TypeScript
// types.ts
import type { InjectionKey, Ref } from 'vue'
export interface User {
name: string
email: string
}
export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme')
export const UserKey: InjectionKey<Ref<User>> = Symbol('user')
<!-- App.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import { ThemeKey, UserKey, type User } from './types'
const theme = ref<string>('dark')
const user = ref<User>({ name: 'Иван', email: 'ivan@example.com' })
provide(ThemeKey, theme)
provide(UserKey, user)
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { ThemeKey, UserKey } from './types'
const theme = inject(ThemeKey)
// theme имеет тип Ref<string> | undefined
const user = inject(UserKey)
// user имеет тип Ref<User> | undefined
</script>
Работа на уровне приложения
Глобальный provide
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.provide('apiUrl', 'https://api.example.com')
app.provide('appName', 'My App')
app.mount('#app')
<!-- AnyComponent.vue -->
<script setup>
import { inject } from 'vue'
const apiUrl = inject('apiUrl')
const appName = inject('appName')
console.log(apiUrl) // 'https://api.example.com'
console.log(appName) // 'My App'
</script>
Практические примеры
Управление темой
<!-- App.vue -->
<template>
<div :class="`theme-${theme}`">
<ThemeToggle />
<Content />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import ThemeToggle from './ThemeToggle.vue'
import Content from './Content.vue'
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
<!-- ThemeToggle.vue -->
<template>
<button @click="toggleTheme">
Тема: {{ theme }}
</button>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
Контекст модального окна
<!-- Modal.vue -->
<template>
<div v-if="isOpen" class="modal">
<slot />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
const isOpen = ref(false)
const open = () => {
isOpen.value = true
}
const close = () => {
isOpen.value = false
}
provide('modal', {
isOpen,
open,
close
})
</script>
<!-- ModalTrigger.vue -->
<template>
<button @click="modal.open">
Открыть модальное окно
</button>
</template>
<script setup>
import { inject } from 'vue'
const modal = inject('modal')
</script>
API клиент
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
class ApiClient {
async get(url) {
const response = await fetch(`https://api.example.com${url}`)
return response.json()
}
async post(url, data) {
const response = await fetch(`https://api.example.com${url}`, {
method: 'POST',
body: JSON.stringify(data)
})
return response.json()
}
}
const api = new ApiClient()
provide('api', api)
</script>
<!-- UsersList.vue -->
<template>
<div v-for="user in users" :key="user.id">
{{ user.name }}
</div>
</template>
<script setup>
import { inject, ref, onMounted } from 'vue'
const api = inject('api')
const users = ref([])
onMounted(async () => {
users.value = await api.get('/users')
})
</script>
provide/inject vs props
Когда использовать props
<!-- Прямая связь родитель-ребёнок -->
<ChildComponent :title="title" :count="count" />
Используйте props когда:
- Компоненты напрямую связаны
- Нужна явная связь данных
- Простая иерархия (1-2 уровня)
Когда использовать provide/inject
<!-- Глубокая иерархия -->
<GrandParent>
<Parent>
<Child>
<DeepChild /> <!-- Нужны данные из GrandParent -->
</Child>
</Parent>
</GrandParent>
Используйте provide/inject когда:
- Глубокая иерархия компонентов
- Много промежуточных компонентов
- Глобальный контекст (тема, локаль, API)
Ограничения и особенности
Односторонняя передача данных
<!-- Parent.vue -->
<script setup>
import { provide, readonly, ref } from 'vue'
const count = ref(0)
// Защита от изменений в дочерних компонентах
provide('count', readonly(count))
</script>
Нельзя использовать до setup
<script setup>
// Правильно
const theme = inject('theme')
// Неправильно - inject вне setup
setTimeout(() => {
const theme = inject('theme') // Ошибка!
}, 1000)
</script>
Частые ошибки
Передача не-реактивного значения
<!-- Неправильно -->
<script setup>
import { provide, ref } from 'vue'
const count = ref(0)
provide('count', count.value) // Передаётся только значение, не ref!
</script>
<!-- Правильно -->
<script setup>
import { provide, ref } from 'vue'
const count = ref(0)
provide('count', count) // Передаётся ref
</script>
Изменение данных в дочернем компоненте
<!-- Неправильно -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
// Напрямую изменять не рекомендуется
theme.value = 'dark'
</script>
<!-- Правильно - передавайте функцию для изменения -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const setTheme = inject('setTheme')
setTheme('dark')
</script>
Вывод
provide/inject:
- Передача данных через иерархию без props
- Решает проблему prop drilling
- Поддерживает реактивность
- Можно использовать Symbol для ключей
- Подходит для глобального состояния (тема, локаль)
- Не заменяет Vuex/Pinia для сложного state management
На собеседовании:
Важно уметь:
- Объяснить, что такое provide/inject и зачем они нужны
- Рассказать о проблеме prop drilling
- Показать, как работает реактивность с provide/inject
- Объяснить разницу между provide/inject и props
- Привести примеры использования (тема, API клиент)