Что такое слоты во Vue: default, именованные и scoped?

MiddleVue.js #vue #slots #templates

Вопрос

Что такое слоты во Vue, чем отличаются default-слот, именованные слоты и scoped-слоты?

Короткий ответ

Слоты — это механизм, позволяющий родительскому компоненту передавать разметку (а не только данные) внутрь дочернего компонента, а дочерний компонент решает, куда её вставить. Default-слот — один безымянный слот для основного содержимого; именованные слоты позволяют передавать несколько независимых кусков разметки в разные места шаблона; scoped-слоты дополнительно дают возможность дочернему компоненту передать свои данные обратно в содержимое слота, которое рендерит родитель.

Подробный ответ

Если props передают данные, а events — сигналы о событиях, то слоты передают разметку. Дочерний компонент объявляет в своём шаблоне место <slot>, куда будет вставлено содержимое, переданное родителем между открывающим и закрывающим тегом компонента.

  • Default-слот. <slot></slot> в дочернем компоненте — родитель просто пишет содержимое между тегами: <Card>Текст</Card>. Если родитель ничего не передал, можно указать содержимое по умолчанию внутри <slot>Заглушка</slot>.
  • Именованные слоты (named slots). Когда нужно несколько независимых мест для вставки (например, шапка/тело/подвал карточки), слоты именуются: <slot name="header" />. Родитель указывает, куда какая разметка идёт, через <template #header>.
  • Scoped-слоты. Обычно данные текут от родителя к содержимому слота, но иногда именно дочерний компонент владеет данными, которые нужно показать в кастомном виде (например, компонент списка знает текущий элемент итерации, а как его отрисовать — решает родитель). Дочерний компонент передаёт данные через атрибуты на <slot>: <slot :item="item" />. Родитель получает их через <template #default="{ item }">, деструктурируя объект слот-пропсов.

Слоты — ключевой инструмент для написания переиспользуемых, гибких по разметке компонентов (модалки, карточки, списки, layout'ы), где сама структура и логика едины, а конкретное содержимое каждый раз разное.

Пример

<!-- BaseCard.vue: именованные + scoped слоты -->
<script setup>
defineProps({
  items: { type: Array, required: true }
})
</script>

<template>
  <div class="card">
    <header class="card__header">
      <!-- именованный слот с содержимым по умолчанию -->
      <slot name="header">Без заголовка</slot>
    </header>

    <ul class="card__body">
      <!-- scoped slot: дочерний компонент отдаёт item и index наружу -->
      <li v-for="(item, index) in items" :key="item.id">
        <slot :item="item" :index="index">
          {{ item.name }} <!-- поведение по умолчанию -->
        </slot>
      </li>
    </ul>
  </div>
</template>
<!-- Parent.vue -->
<script setup>
import BaseCard from './BaseCard.vue'

const users = [
  { id: 1, name: 'Alice', active: true },
  { id: 2, name: 'Bob', active: false }
]
</script>

<template>
  <BaseCard :items="users">
    <template #header>
      <h2>Список пользователей</h2>
    </template>

    <!-- scoped-слот: получаем item из дочернего компонента -->
    <template #default="{ item }">
      <strong :class="{ online: item.active }">{{ item.name }}</strong>
    </template>
  </BaseCard>
</template>

Дополнительные вопросы

  • Чем v-slot (#) отличается от обычного v-bind, и почему у слотов своя область видимости?
  • Как проверить, передал ли родитель содержимое в конкретный слот ($slots)?
  • В каком случае вместо scoped-слота лучше использовать обычный проп-функцию (render prop)?