Skip to main content

Provide/inject у Vue.js

provide/inject - механізм у Vue.js для передачі даних від батьківського компонента до будь-якого нащадка без явного прокидання через проміжні компоненти як пропси.

Теорія

TL;DR

  • Аналогія: як спільний локер в офісі - батько кладе щось всередину (provide), будь-який нащадок бере за ключем (inject), ніхто посередині нічого не чіпає
  • Головна різниця: пропси вимагають від кожного проміжного компонента явно приймати і передавати дані; provide/inject оминає їх повністю
  • Реактивність працює: якщо надати ref, нащадки отримають той самий об'єкт ref, а не копію - зміни поширюються автоматично
  • Правило вибору: provide/inject для теми, авторизації, локалі, конфігурації; пропси для прямого зв'язку батько-дитина; Pinia для загального стану застосунку

Швидкий приклад

javascript
// App.vue - батьківський компонент <script setup> import { provide, ref } from 'vue' const user = ref({ name: 'Alice', role: 'admin' }) provide('currentUser', user) // доступно для ВСІХ нащадків </script> // DeepChild.vue - може бути на 10 рівнів глибше, жодних проміжних пропсів <script setup> import { inject } from 'vue' const user = inject('currentUser', ref({ name: 'Guest' })) // другий аргумент = дефолт console.log(user.value.name) // 'Alice' </script>

Нащадок отримує той самий об'єкт ref, а не копію. Змінюєш в одному місці - всі передплатники оновлюються.

Ключова різниця від пропсів

Пропси створюють явний ланцюжок: кожен компонент у дереві повинен оголосити пропс, прийняти і передати далі. При п'яти рівнях вкладеності це п'ять компонентів, які несуть дані яких самі не використовують. provide/inject розриває цей ланцюжок. Батько оголошує один раз; будь-який нащадок на будь-якій глибині отримує дані напряму. Проміжні компоненти залишаються чистими.

Коли що використовувати

  • provide/inject: перемикання теми, стан авторизації, глобальна конфігурація, feature flags, локаль, спільні сервіси як API-клієнт
  • Пропси: прямий зв'язок батько-дитина, дані яким потрібні тільки 1-2 рівні, коли важлива явна і прозора передача
  • Pinia: загальний стан застосунку, який потребує devtools, time-travel debugging або складних мутацій

Як це працює всередині

Vue підтримує окремий provide-ланцюжок для кожного екземпляра компонента. Коли компонент викликає provide(key, value), Vue зберігає значення в цьому екземплярі. Коли нащадок викликає inject(key), Vue йде вгору по дереву і шукає найближчого предка, який надав цей ключ. Якщо два предки надають однаковий ключ, перемагає ближчий - це дозволяє перевизначати значення у піддеревах, наприклад для вкладеної теми. Якщо нічого не знайдено, inject повертає вказане дефолтне значення або undefined.

provide на рівні застосунку

Можна надавати значення на рівні всього застосунку - тоді воно буде доступне скрізь:

javascript
// main.js import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.provide('apiUrl', 'https://api.example.com') app.mount('#app')
javascript
// AnyComponent.vue <script setup> import { inject } from 'vue' const apiUrl = inject('apiUrl') // 'https://api.example.com' </script>

Саме так Vue Router і Vuetify роблять свої внутрішні дані доступними без явних імпортів у кожен файл.

Символьні ключі

Рядкові ключі можуть конфліктувати, якщо різні частини коду випадково використовують однакову назву. Symbol вирішує це:

javascript
// keys.js export const ThemeKey = Symbol('theme') export const UserKey = Symbol('user') // Parent.vue import { ThemeKey, UserKey } from './keys' provide(ThemeKey, theme) provide(UserKey, user) // Child.vue import { ThemeKey } from './keys' const theme = inject(ThemeKey) // помилки в рядковому ключі неможливі

У TypeScript пари Symbol з InjectionKey<T> дають повну типізацію:

typescript
// keys.ts import type { InjectionKey, Ref } from 'vue' export const UserKey: InjectionKey<Ref<{ name: string }>> = Symbol('user') // inject(UserKey) автоматично повертає Ref<{ name: string }> | undefined

Типові помилки

Помилка 1: передача значення замість ref

javascript
// Неправильно - передається знімок, а не реактивний ref const count = ref(0) provide('count', count.value) // number, а не Ref<number> // Правильно provide('count', count)

Якщо передати count.value, нащадок отримає 0 як звичайне число. Подальші зміни до нього не дійдуть.

Помилка 2: пряма мутація даних у дочірньому компоненті

javascript
// Працює, але не рекомендується - незрозуміло хто власник даних const config = inject('config') config.value.apiUrl = 'https://new.api.com' // Краще - разом з даними надати функцію оновлення // У батьку: const updateConfig = (key, value) => { config.value[key] = value } provide('config', config) provide('updateConfig', updateConfig) // У дочірньому: const updateConfig = inject('updateConfig') updateConfig('apiUrl', 'https://new.api.com') // явно і прозоро

Помилка 3: відсутність дефолтного значення

javascript
// Якщо батько ніколи не надав 'user', це впаде в runtime const user = inject('user') console.log(user.value.name) // TypeError: Cannot read properties of undefined // Безпечний варіант const user = inject('user', ref({ name: 'Guest' }))

Завжди додавай дефолт, коли батьківський компонент, що надає дані, може бути відсутній у дереві.

Помилка 4: очікування ізоляції замість спільного посилання

javascript
// Батько const count = ref(0) provide('count', count) // Child A const count = inject('count') count.value++ // збільшує спільний ref // Child B const count = inject('count') console.log(count.value) // 1, а не 0 - той самий об'єкт

Це правильна поведінка, але вона дивує тих, хто очікує копію. Якщо потрібна ізоляція, надай окремі ref-и.

Помилка 5: циклічні provide-ланцюжки

Якщо дочірній компонент повторно надає дані, які він отримав від батька, а онук отримує від обох, виникає прихована залежність. Зміна структури дерева ламає онука без явної помилки. Тримай provide/inject-ланцюжки лінійними.

Де зустрічається в реальних проєктах

  • Vue Router надає метадані маршруту вкладеним компонентам - саме так працює useRoute() всередині
  • Vuetify надає конфігурацію теми всім дочірнім компонентам без prop drilling
  • Vee-Validate - батьківська форма надає контекст валідації (validation context) кожному вкладеному полю
  • Pinia всередині використовує provide/inject щоб зробити сторінки доступними по всьому застосунку

Найчастіше я використовував provide/inject для контексту авторизації - надаючи поточного користувача і функцію виходу на рівні застосунку, щоб будь-який компонент міг їх отримати без пропсів.

Питання на співбесіді

Q: Що відбувається, якщо два предки надають однаковий ключ?
A: Перемагає найближчий предок. Нащадки не можуть отримати значення від вищих предків в обхід ближчого. Це дозволяє перевизначати значення у піддеревах, наприклад надати іншу тему всередині модального вікна.

Q: Яка різниця між provide/inject і Pinia?
A: provide/inject обмежений підгіллям компонентного дерева. Pinia - це глобальні синглтони зі підтримкою devtools, time-travel debugging і структурованими патернами мутацій. Використовуй provide/inject для локального спільного контексту; Pinia - коли потрібен загальний стан по всьому застосунку.

Q: Що станеться, якщо inject не знайде відповідного provide?
A: inject повертає undefined або дефолтне значення, яке ти передав другим аргументом. Тому відсутність дефолту - часте джерело runtime-помилок.

Q: Як правильно типізувати provide/inject у TypeScript?
A: Використовуй InjectionKey<T> з Vue: const UserKey: InjectionKey<Ref<User>> = Symbol('user'). Тоді inject(UserKey) автоматично повертає Ref<User> | undefined. Додай дефолт щоб звузити тип до Ref<User>.

Q: (Senior) Як побудувати систему багаторівневих тем за допомогою provide/inject?
A: Кожен компонент-границя теми надає власний ключ теми. Дочірні компоненти отримують дані від найближчого предка. Оскільки Vue завжди вирішує до найближчого провайдера, вкладення компонента темної теми у світлий застосунок просто працює - він перекриває значення батька для всіх своїх нащадків.

Приклади

Система тем через provide/inject

javascript
// ThemeProvider.vue <template> <div :class="`theme-${theme}`"> <button @click="toggleTheme"> Переключити на {{ theme === 'dark' ? 'light' : 'dark' }} </button> <slot /> </div> </template> <script setup> import { provide, ref } from 'vue' const theme = ref('dark') const toggleTheme = () => { theme.value = theme.value === 'dark' ? 'light' : 'dark' } // Надаємо і стан, і функцію зміни provide('theme', theme) provide('toggleTheme', toggleTheme) </script>
javascript
// DeepButton.vue - 8 рівнів вкладеності, жодного prop drilling <template> <button @click="toggleTheme" :class="`btn-${theme}`"> Поточна тема: {{ theme }} </button> </template> <script setup> import { inject } from 'vue' const theme = inject('theme') const toggleTheme = inject('toggleTheme') // Клік оновлює тему глобально - всі нащадки перерендеряться </script>

Надання функції разом з даними зберігає логіку мутацій у компоненті, якому належить стан. Скільки б нащадків не викликало її - джерело правди одне.

Типобезпечне отримання через символьні ключі

typescript
// keys.ts import type { InjectionKey, Ref } from 'vue' export interface User { id: number name: string role: 'admin' | 'viewer' } export const UserKey: InjectionKey<Ref<User>> = Symbol('user') // Parent.vue import { provide, ref } from 'vue' import { UserKey } from './keys' const user = ref<User>({ id: 1, name: 'Alice', role: 'admin' }) provide(UserKey, user) // Child.vue - TypeScript знає точну структуру даних import { inject } from 'vue' import { UserKey } from './keys' const user = inject(UserKey) // Ref<User> | undefined if (user) { console.log(user.value.role) // TypeScript підтверджує що 'role' існує }

Рядкові ключі працюють, але при inject дають тип unknown. Symbol з InjectionKey<T> дають повний тип і виловлюють помилки імпорту на етапі компіляції, до запуску коду.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?