13. Provide / Inject
🔌 Provide / Inject в Vue 3
Заголовок раздела «🔌 Provide / Inject в Vue 3»Imagine у тебя компонент App → Layout → Sidebar → UserAvatar. Хочешь передать данные пользователя в UserAvatar. Прокидывать через все 3 уровня через props — это prop drilling, и это больно 😩
provide / inject — это как “телепорт для данных”. Родитель делает provide, а любой потомок на любой глубине делает inject и получает данные. Никаких цепочек пропсов! 🚀
Базовый синтаксис
Заголовок раздела «Базовый синтаксис»<!-- Родительский компонент (или App.vue) --><script setup lang="ts">import { provide, ref } from 'vue'
const user = ref({ name: 'Яша', role: 'admin' })const theme = ref<'light' | 'dark'>('dark')
// Делаем данные доступными для всех потомковprovide('user', user)provide('theme', theme)provide('appVersion', '1.0.0') // Можно и простые значения</script><!-- Дочерний компонент (на любой глубине!) --><script setup lang="ts">import { inject, ref } from 'vue'
// Получаем данные от предкаconst user = inject('user') // Тип: unknown 😢const theme = inject('theme')const version = inject('appVersion')
// С дефолтным значением (если provide не было)const locale = inject('locale', 'ru')</script>
<template> <div>Привет, {{ user?.name }}!</div></template>⚠️ Проблема:
inject('user')возвращает типunknown. Нет TypeScript поддержки! Для этого естьInjectionKey.
InjectionKey — типизированный provide/inject
Заголовок раздела «InjectionKey — типизированный provide/inject»InjectionKey<T> — это специальный Symbol с дженериком для типобезопасного provide/inject:
// keys.ts — общие ключи для всего приложенияimport type { InjectionKey, Ref } from 'vue'
interface User { id: number name: string email: string role: 'admin' | 'user' | 'guest'}
interface Theme { mode: 'light' | 'dark' primary: string}
// Символ-ключ с типом!export const UserKey = Symbol() as InjectionKey<Ref<User>>export const ThemeKey = Symbol() as InjectionKey<Ref<Theme>>export const LocaleKey = Symbol() as InjectionKey<Ref<string>>export const RouterKey = Symbol() as InjectionKey<ReturnType<typeof useRouter>><!-- Провайдер с типами --><script setup lang="ts">import { provide, ref } from 'vue'import { UserKey, ThemeKey } from '@/keys'
const user = ref<User>({ id: 1, name: 'Яша', role: 'admin'})
const theme = ref<Theme>({ mode: 'dark', primary: '#42b883'})
provide(UserKey, user) // TypeScript знает тип!provide(ThemeKey, theme)</script><!-- Потребитель с типами --><script setup lang="ts">import { inject } from 'vue'import { UserKey, ThemeKey } from '@/keys'
// user имеет тип Ref<User> — автодополнение работает! 🎉const user = inject(UserKey)const theme = inject(ThemeKey)
// С обязательным дефолтным значениемconst locale = inject(LocaleKey, ref('ru'))
// Если значение гарантированно есть — используй !// Но лучше всегда давать default</script>
<template> <div :style="{ color: theme?.primary }"> {{ user?.name }} ({{ user?.role }}) </div></template>App-level Provide
Заголовок раздела «App-level Provide»Можно делать provide на уровне всего приложения — идеально для глобальных сервисов:
import { createApp } from 'vue'import App from './App.vue'import { ApiService } from './services/api'import { I18n } from './i18n'
const app = createApp(App)
// Глобальный provide — доступен везде в приложенииapp.provide('apiService', new ApiService())app.provide('i18n', new I18n('ru'))app.provide('featureFlags', { darkMode: true, betaFeatures: false,})
app.mount('#app')Реактивность в provide/inject
Заголовок раздела «Реактивность в provide/inject»Если передаёшь ref или reactive — изменения автоматически отразятся у всех потребителей:
<script setup lang="ts">import { provide, ref, computed } from 'vue'import type { InjectionKey, Ref } from 'vue'
interface ThemeContext { isDark: Ref<boolean> toggleTheme: () => void primary: Ref<string>}
export const ThemeContextKey = Symbol() as InjectionKey<ThemeContext>
const isDark = ref(true)const primary = computed(() => isDark.value ? '#42b883' : '#2196f3')
// Предоставляем как данные, так и методы!provide(ThemeContextKey, { isDark, toggleTheme: () => { isDark.value = !isDark.value }, primary,})</script>
<template> <div :class="isDark ? 'dark' : 'light'"> <slot /> </div></template><!-- Кнопка где-то глубоко в дереве --><script setup lang="ts">import { inject } from 'vue'import { ThemeContextKey } from './ThemeProvider.vue'
const theme = inject(ThemeContextKey)// theme?.isDark — реактивно!// При смене темы компонент автоматически обновится</script>
<template> <button :style="{ background: theme?.primary.value }" @click="theme?.toggleTheme()" > {{ theme?.isDark.value ? '🌙 Тёмная' : '☀️ Светлая' }} </button></template>readonly() для безопасности
Заголовок раздела «readonly() для безопасности»Хорошей практикой является запрет мутаций из дочерних компонентов. Дочерние должны только читать, а изменения — через методы от родителя:
<script setup lang="ts">import { provide, ref, readonly } from 'vue'
const count = ref(0)
provide('count', readonly(count)) // Дочерние могут только читать!provide('increment', () => count.value++) // Мутация только через методprovide('decrement', () => count.value--)</script><!-- Дочерний компонент --><script setup lang="ts">import { inject } from 'vue'
const count = inject<Readonly<Ref<number>>>('count')const increment = inject<() => void>('increment')const decrement = inject<() => void>('decrement')</script>
<template> <div> <button @click="decrement">−</button> {{ count }} <button @click="increment">+</button> </div></template>Реальный пример: Form контекст
Заголовок раздела «Реальный пример: Form контекст»Паттерн provide/inject очень популярен для составных компонентов форм:
<script setup lang="ts">import { provide, reactive, ref } from 'vue'import type { InjectionKey } from 'vue'
interface FormField { value: unknown error: string | null touched: boolean}
interface FormContext { registerField: (name: string) => void getField: (name: string) => FormField | undefined setError: (name: string, error: string | null) => void submitting: Ref<boolean>}
export const FormKey = Symbol() as InjectionKey<FormContext>
const fields = reactive<Record<string, FormField>>({})const submitting = ref(false)
provide(FormKey, { registerField(name) { if (!fields[name]) { fields[name] = { value: '', error: null, touched: false } } }, getField: (name) => fields[name], setError: (name, error) => { if (fields[name]) fields[name].error = error }, submitting,})
async function handleSubmit() { submitting.value = true // Валидация... submitting.value = false}</script>
<template> <form @submit.prevent="handleSubmit"> <slot /> </form></template><!-- FormInput.vue — использует контекст формы --><script setup lang="ts">import { inject, onMounted } from 'vue'import { FormKey } from './Form.vue'
const props = defineProps<{ name: string label: string}>()
const form = inject(FormKey)
onMounted(() => { form?.registerField(props.name)})
const field = computed(() => form?.getField(props.name))</script>
<template> <div class="field"> <label>{{ label }}</label> <input :value="field?.value as string" @input="/* обновить значение */" :class="{ error: field?.error }" /> <span v-if="field?.error" class="error">{{ field.error }}</span> </div></template>Когда использовать provide/inject vs props vs Pinia
Заголовок раздела «Когда использовать provide/inject vs props vs Pinia»┌─────────────────┬──────────────────────────────────────────────────┐│ Ситуация │ Решение │├─────────────────┼──────────────────────────────────────────────────┤│ Данные между │ Props — прямой и понятный способ ││ родитель-ребёнок│ │├─────────────────┼──────────────────────────────────────────────────┤│ Глубокое дерево │ Provide/Inject — избегаем prop drilling ││ компонентов │ │├─────────────────┼──────────────────────────────────────────────────┤│ Составные │ Provide/Inject — Form+Input, Tabs+Tab, ││ компоненты │ Accordion+Item паттерны │├─────────────────┼──────────────────────────────────────────────────┤│ Глобальное │ Pinia — управление состоянием, ││ состояние │ девтулс, SSR поддержка │├─────────────────┼──────────────────────────────────────────────────┤│ Сервисы │ app.provide() — ApiService, i18n, router ││ (singleton) │ │└─────────────────┴──────────────────────────────────────────────────┘