Что такое динамические компоненты и паттерн <component :is="">?
<component :is="X"> рендерит компонент или HTML-тег, переданный как объект или строка. Используется для вкладок, визардов, полиморфных блоков. Для сохранения состояния оборачивается в KeepAlive; объект компонента храните в shallowRef.
Что такое динамические компоненты
Динамический компонент — механизм Vue, позволяющий рендерить разные компоненты в одной точке шаблона на основании реактивного значения. Специальный элемент <component :is="..."> принимает строку с именем зарегистрированного компонента, объект компонента или результат defineAsyncComponent. При смене значения is Vue размонтирует предыдущий компонент и монтирует новый.
Типичные применения: вкладки, шаги визарда, рендер блоков статьи по типу (текст / изображение / видео / цитата), переключение форм входа/регистрации.
Базовый пример с вкладками
// TabView.vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import TabHome from './TabHome.vue'
import TabProfile from './TabProfile.vue'
import TabSettings from './TabSettings.vue'
const tabs = {
home: TabHome,
profile: TabProfile,
settings: TabSettings,
} as const
type TabKey = keyof typeof tabs
const current = shallowRef<TabKey>('home')
</script>
<template>
<nav>
<button
v-for="key in (Object.keys(tabs) as TabKey[])"
:key="key"
:class="{ active: current === key }"
@click="current = key"
>
{{ key }}
</button>
</nav>
<KeepAlive :include="['TabHome', 'TabProfile']">
<component :is="tabs[current]" />
</KeepAlive>
</template>
Почему shallowRef, а не ref? Объект компонента (TabHome) при хранении в обычном ref стал бы глубоко реактивным — Vue обошёл бы все его свойства прокси-оборачиванием. Это бесполезно и замедляет инициализацию. shallowRef хранит ссылку без проксирования вложенных полей. Альтернатива — markRaw(TabHome).
Динамический тег HTML
<script setup lang="ts">
const props = defineProps<{ level: 1 | 2 | 3 | 4 }>()
</script>
<template>
<component :is="`h${level}`">
<slot />
</component>
</template>
Передача строки вместо объекта компонента заставляет Vue рендерить нативный HTML-тег. Удобно для семантически корректных заголовков или полиморфных обёрток (div / section / article).
Асинхронные компоненты и Suspense
<script setup lang="ts">
import { defineAsyncComponent, shallowRef } from 'vue'
const AsyncChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: () => import('./Spinner.vue'),
errorComponent: () => import('./ErrorBanner.vue'),
delay: 200,
timeout: 5000,
})
const current = shallowRef(AsyncChart)
</script>
<template>
<Suspense>
<component :is="current" />
<template #fallback>Загрузка...</template>
</Suspense>
</template>
KeepAlive: сохранение состояния
Без <KeepAlive> каждый переход уничтожает и пересоздаёт состояние компонента. KeepAlive кеширует экземпляры; компонент получает хуки onActivated / onDeactivated вместо onMounted / onUnmounted. Атрибут :include принимает массив имён, :max ограничивает размер LRU-кеша.
Подводные камни
- Объект компонента в обычном
ref— глубокая реактивность без пользы; всегда используйтеshallowRefилиmarkRaw. - При передаче строки (имени компонента) Vue ищет его в локальных импортах и глобальных регистрациях; в DEV-режиме выведет предупреждение если не найдёт.
- При SSR (Nuxt) убедитесь, что значение
currentодинаково на сервере и клиенте при гидрации, иначе получите hydration mismatch. - Асинхронные компоненты без
SuspenseилиloadingComponentпокажут пустоту во время загрузки — всегда обрабатывайте состояние загрузки. KeepAliveудерживает экземпляры компонентов в памяти — не кешируйте тяжёлые компоненты без ограничения:max.- Props и события пробрасываются на текущий компонент автоматически, но TypeScript не знает тип текущего компонента — типизируйте явно при необходимости.
- При динамическом теге (
:is="tag") Vue не проверяет, что строка является валидным HTML-тегом — некорректные значения создадут кастомный элемент без ошибки в рантайме. - Не путайте
<component :is>сv-if/v-else-if: для двух-трёх фиксированных вариантов условный рендеринг нагляднее; динамический компонент выигрывает при расширяемом наборе вариантов.
Common mistakes
- Хранить компонент-объект в
refи удивляться, что Vue его проксирует. - Забывать
<KeepAlive>и каждый раз терять состояние вкладки. - Передавать в
isстроку с именем, не зарегистрировав компонент глобально/локально. - Сочетать
:isсv-if, когда достаточно одного из них.
What the interviewer is testing
- Знает синтаксис
<component :is>и оба варианта значения (строка или объект). - Понимает связь с
<KeepAlive>. - Знает про
shallowRef/markRawдля хранения компонентов. - Видит сценарии: вкладки, динамические форм-контролы, мастер-визарды.