Vue.jsMiddleTechnical

Что такое динамические компоненты и паттерн <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 для хранения компонентов.
  • Видит сценарии: вкладки, динамические форм-контролы, мастер-визарды.

Sources

Related topics