Hack Frontend Community

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 клиент)