Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Provide/inject у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Provide/inject** у Vue.js передає дані від батька до будь-якого нащадка без prop drilling через проміжні компоненти. ```javascript // Батьківський компонент const user = ref({ name: 'Alice' }) provide('user', user) // Будь-який нащадок (проміжні пропси не потрібні) const user = inject('user', ref({ name: 'Guest' })) ``` **Ключове:** надавай `ref`, а не його `.value`, інакше нащадок отримає статичний знімок без реактивності.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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>` дають повний тип і виловлюють помилки імпорту на етапі компіляції, до запуску коду.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.