Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Динамічні компоненти у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Динамічні компоненти** дають змогу рендерити різні Vue-компоненти в одному місці DOM через `<component :is="...">`. Коли `:is` змінюється, Vue демонтує старий компонент і монтує новий. ```vue <component :is="currentTab" /> ``` Зберігай посилання через `shallowRef`. Для збереження стану при перемиканні - `<KeepAlive>`. **Ключове:** передавай об'єкт компонента, а не рядок.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Динамічні компоненти** (dynamic components) дають змогу замінювати один Vue-компонент іншим в одному місці DOM - достатньо змінити те, на що вказує атрибут `:is` у `<component>`. ## Теорія ### TL;DR - Уяви пульт від телевізора: `<component>` - це екран, `:is` - кнопка перемикання каналів. - Коли `:is` змінюється, Vue демонтує старий компонент і монтує новий на тому ж місці. - Передавай об'єкт компонента в `:is`, а не рядок. Рядок нічого не рендерить і не видає помилок. - Обгорни в `<KeepAlive>`, якщо хочеш зберегти стан при поверненні до компонента. - Зберігай активний компонент через `shallowRef`, а не `ref`. ### Швидкий приклад ```vue <script setup> import { shallowRef } from 'vue' import Home from './Home.vue' import Settings from './Settings.vue' // shallowRef стежить тільки за посиланням, а не за вмістом об'єкта компонента const activeTab = shallowRef(Home) const tabs = { home: Home, settings: Settings } </script> <template> <button @click="activeTab = tabs.home">Головна</button> <button @click="activeTab = tabs.settings">Налаштування</button> <!-- Vue демонтує Home і монтує Settings коли activeTab змінюється --> <component :is="activeTab" /> </template> ``` Натискаєш «Налаштування» - Vue знищує екземпляр `Home` і створює свіжий `Settings` на тому ж місці. ### Головна різниця Динамічні компоненти вирішують іншу задачу, ніж `v-if`. З `v-if` ти пишеш окрему гілку для кожного компонента, і шаблон росте з кожним новим варіантом. З `<component :is>` ти просто додаєш запис в об'єкт, а шаблон не змінюється. Починаючи з трьох компонентів ланцюжки `v-if` стають незручними. Саме тоді `<component :is>` показує свою цінність. ### Коли використовувати - **Таби:** перемикання між видами без перезавантаження сторінки - **Багатокрокові форми:** кожен крок - окремий компонент - **Системи плагінів:** завантаження компонентів на основі конфігурації або даних - **Адмін-панелі:** різні типи блоків (графіки, таблиці, форми) за типом даних - **CMS-блоки:** hero, gallery, testimonial з конфігураційного масиву Для одного-двох варіантів простіше залишити `v-if`. Він зрозуміліший і явніший у таких випадках. ### Як це працює всередині Коли `:is` змінюється, система реактивності Vue фіксує нове значення і запускає оновлення компонента. Старий компонент отримує хуки `beforeUnmount` і `unmounted`, його DOM-вузли видаляються, потім монтується новий з `beforeMount` і `mounted`. Якщо обгорнути `<component>` в `<KeepAlive>`, Vue кешує старий екземпляр замість того, щоб знищувати його. Повернення до нього миттєве, і весь стан (введені дані, результати запитів) зберігається. ### Типові помилки **Передача рядка замість об'єкта компонента** ```vue <!-- НЕПРАВИЛЬНО: Vue шукає 'HomeView' в глобальному реєстрі. Нічого не рендериться. Жодної помилки. --> <script setup> import { ref } from 'vue' const currentView = ref('HomeView') </script> <template> <component :is="currentView" /> </template> ``` ```vue <!-- ПРАВИЛЬНО: передай сам об'єкт компонента --> <script setup> import { shallowRef } from 'vue' import HomeView from './HomeView.vue' import SettingsView from './SettingsView.vue' const views = { home: HomeView, settings: SettingsView } const currentView = shallowRef(HomeView) </script> <template> <component :is="currentView" /> </template> ``` Це тихий баг - нема попередження, нема рендеру. Можна витратити 20 хвилин, перш ніж зрозумієш що відбувається. **Втрата стану без KeepAlive** ```vue <!-- НЕПРАВИЛЬНО: користувач заповнив форму, переключив таб, повернувся - форма порожня --> <component :is="currentTab" /> <!-- ПРАВИЛЬНО: дані форми виживають при перемиканні табів --> <KeepAlive> <component :is="currentTab" /> </KeepAlive> ``` При кожній зміні `:is` Vue знищує старий екземпляр. Дані в полях, позиція скролу, результати API-запитів - все зникає. Користувачі одразу це помічають і сприймають як баг. **Таймери продовжують працювати в кешованих компонентах** ```vue <!-- НЕПРАВИЛЬНО: таймер працює навіть коли таб прихований --> <script setup> import { onMounted } from 'vue' onMounted(() => { setInterval(() => fetch('/api/updates'), 5000) }) </script> ``` ```vue <!-- ПРАВИЛЬНО: зупиняй при приховуванні, відновлюй при показі --> <script setup> import { onMounted, onActivated, onDeactivated } from 'vue' let timerId onMounted(() => { timerId = setInterval(() => fetch('/api/updates'), 5000) }) onDeactivated(() => clearInterval(timerId)) onActivated(() => { timerId = setInterval(() => fetch('/api/updates'), 5000) }) </script> ``` З `<KeepAlive>` компонент залишається в пам'яті. Його таймери і підписки продовжують виконуватися. П'ять кешованих табів - це п'ять паралельних polling-циклів у фоні. **ref замість shallowRef** ```vue <script setup> // НЕПРАВИЛЬНО: ref глибоко спостерігає за об'єктом компонента - зайві витрати const current = ref(MyComponent) // ПРАВИЛЬНО: shallowRef стежить тільки за посиланням, не за вмістом const current = shallowRef(MyComponent) </script> ``` Об'єкти компонентів можуть бути великими. Глибока реактивність на них додає роботу, яка Vue не потрібна. ### Де зустрічається - Vue Router використовує цей патерн всередині `<RouterView>` для рендеру компонентів маршрутів - Nuxt поєднує динамічні компоненти з `<ClientOnly>` для пропуску SSR на певних видах - Конструктори форм рендерять типи полів (text, select, checkbox) з конфігураційного масиву - В e-commerce сторінки товарів перемикаються між виглядом для цифрових і фізичних продуктів ### Питання на співбесіді **Q:** Яка різниця між `<component :is>` і `v-if` для перемикання видів? **A:** `v-if` простіший для одного-двох варіантів. `<component :is>` краще масштабується при трьох і більше опціях, бо шаблон не росте. Обидва демонтують старий компонент при зміні умови. **Q:** Коли використовувати `<KeepAlive>`, а коли дозволити компоненту демонтуватись? **A:** `<KeepAlive>` потрібен, коли компонент зберігає стан, що варто зберегти: дані форми, результати запитів, позицію скролу. Без нього - коли хочеш чистий екземпляр щоразу або компонент не має стану. **Q:** Чи можна передавати пропси в динамічний компонент? **A:** Так, так само як в будь-який статичний: `<component :is="current" :user-id="123" @save="handleSave" />`. Пропси і події працюють звичайно. **Q:** (Senior) Як побудувати систему динамічних компонентів, де кожен компонент керує своїм lifecycle, не залежачи від батьківського? **A:** Використовуй `<KeepAlive>` з `onActivated` і `onDeactivated` всередині кожного дочірнього компонента. Батько просто перемикає `:is`. Кожен компонент сам відповідає за налаштування і очищення. Для спільного стану або спільних колбеків - `provide`/`inject`. ## Приклади ### Базовий інтерфейс з табами ```vue <script setup> import { shallowRef } from 'vue' import UserProfile from './UserProfile.vue' import UserSettings from './UserSettings.vue' import UserNotifications from './UserNotifications.vue' const tabs = { profile: UserProfile, settings: UserSettings, notifications: UserNotifications, } const currentTab = shallowRef(UserProfile) </script> <template> <div class="tabs"> <button v-for="(comp, name) in tabs" :key="name" :class="{ active: currentTab === comp }" @click="currentTab = comp" > {{ name }} </button> <!-- Без KeepAlive: перемикання табів знищує старий екземпляр компонента --> <component :is="currentTab" /> </div> </template> ``` Переключись на «settings» і назад на «profile» - компонент профілю монтується з нуля. Будь-які введені дані зникають. Це поведінка за замовчуванням, і інколи саме це і потрібно. ### Конструктор форм з динамічними типами полів ```vue <script setup> import TextInput from './TextInput.vue' import SelectInput from './SelectInput.vue' import CheckboxInput from './CheckboxInput.vue' const fieldComponents = { text: TextInput, select: SelectInput, checkbox: CheckboxInput, } const formFields = [ { type: 'text', name: 'username', label: 'Імʼя користувача' }, { type: 'select', name: 'role', label: 'Роль', options: ['Адмін', 'Користувач'] }, { type: 'checkbox', name: 'active', label: 'Активний акаунт' }, ] </script> <template> <form> <div v-for="field in formFields" :key="field.name"> <component :is="fieldComponents[field.type]" v-bind="field" /> </div> </form> </template> ``` Кожне поле рендерить потрібний компонент на основі свого `type`. Додати новий тип поля - один запис в `fieldComponents` і один об'єкт в `formFields`. Шаблон не змінюється.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.