← Курс/provide / inject#235 из 257+30 XP

provide / inject в Vue 3

Проблема props drilling

Когда компонент глубоко вложен в дерево, передача данных через props на каждом уровне превращается в **props drilling** — громоздкий и хрупкий паттерн:

App → Layout → Sidebar → UserMenu → Avatar

Каждый промежуточный компонент вынужден принимать и передавать props, которые ему самому не нужны.

Решение: provide / inject

Vue предлагает встроенный механизм — родительский компонент **предоставляет** данные (provide), а любой потомок на любой глубине может их **получить** (inject).

// Родительский компонент
import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
provide('userConfig', { locale: 'ru', fontSize: 16 })
// Любой дочерний компонент — даже на 5 уровней глубже
import { inject } from 'vue'

// inject(ключ, значение_по_умолчанию)
const theme = inject('theme', ref('light'))
const userConfig = inject('userConfig')

console.log(theme.value)  // 'dark'

Символы как ключи

Строковые ключи могут конфликтовать в больших приложениях. Лучшая практика — использовать Symbol:

// keys.js — один файл для всех ключей
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const CONFIG_KEY = Symbol('config')
// Родитель
import { THEME_KEY } from './keys.js'
provide(THEME_KEY, theme)

// Потомок
import { THEME_KEY } from './keys.js'
const theme = inject(THEME_KEY)

readonly — защита данных

Чтобы потомки не могли напрямую мутировать предоставленные данные (нарушая однонаправленный поток):

import { provide, ref, readonly } from 'vue'

const count = ref(0)

// Предоставляем readonly-версию
provide('count', readonly(count))

// Предоставляем метод для изменения
provide('incrementCount', () => {
  count.value++  // только родитель управляет состоянием
})

Сравнение с React Context

| | Vue provide/inject | React Context |

|---|---|---|

| Создание | Вызов provide() в компоненте | createContext(defaultValue) |

| Потребление | inject(key) | useContext(MyContext) |

| Реактивность | Встроена через ref/reactive | Нужен useState/useReducer |

| Типизация | Symbol + TypeScript | createContext с типом |

| Несколько провайдеров | Вложение компонентов | Вложение <Context.Provider> |

App-level provide

Для глобальных зависимостей (i18n, router, pinia) используют provide на уровне приложения:

const app = createApp(App)
app.provide('globalConfig', { apiUrl: 'https://api.example.com' })
app.mount('#app')

Примеры

Dependency Injection через замыкания и Map — аналог provide/inject в чистом JS

// Паттерн Dependency Injection через иерархию контекстов.
// Каждый контекст хранит свои значения и ссылку на родителя.

function createContext(parent = null) {
  const store = new Map()

  return {
    // Зарегистрировать значение
    provide(key, value) {
      store.set(key, value)
      return this
    },

    // Получить значение — ищем сначала у себя, потом у родителей
    inject(key, defaultValue = undefined) {
      if (store.has(key)) {
        return store.get(key)
      }
      if (parent) {
        return parent.inject(key, defaultValue)
      }
      return defaultValue
    },

    // Создать дочерний контекст
    createChild() {
      return createContext(this)
    },

    // Отладка
    debug() {
      const own = {}
      store.forEach((v, k) => { own[String(k)] = v })
      return { own, hasParent: parent !== null }
    }
  }
}

// --- Демонстрация ---

// Корневой контекст (аналог app.provide)
const rootCtx = createContext()
rootCtx.provide('theme', 'dark')
rootCtx.provide('locale', 'ru')
rootCtx.provide('apiUrl', 'https://api.example.com')

// Дочерний контекст (аналог компонента-провайдера)
const featureCtx = rootCtx.createChild()
featureCtx.provide('theme', 'light')  // переопределяем тему
featureCtx.provide('featureFlag', true)

// Глубоко вложенный потомок (аналог конечного компонента)
const leafCtx = featureCtx.createChild()

console.log('=== Разрешение зависимостей ===')
console.log('theme у leaf:', leafCtx.inject('theme'))
// 'light' — из featureCtx (ближайший родитель, который provide)

console.log('locale у leaf:', leafCtx.inject('locale'))
// 'ru' — из rootCtx (проксируется через featureCtx)

console.log('apiUrl у leaf:', leafCtx.inject('apiUrl'))
// 'https://api.example.com' — из rootCtx

console.log('featureFlag у leaf:', leafCtx.inject('featureFlag'))
// true — из featureCtx

console.log('missing у leaf:', leafCtx.inject('missing', 'default!'))
// 'default!' — не найдено, использует defaultValue

console.log('\n=== Изолированный контекст ===')
console.log('theme у root:', rootCtx.inject('theme'))
// 'dark' — rootCtx не видит переопределение в featureCtx

console.log('\n=== Отладка ===')
console.log('featureCtx.debug():', featureCtx.debug())