Skip to main content

Динамічні компоненти у Vue.js

Динамічні компоненти (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. Шаблон не змінюється.

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

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

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

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