Олег Марков
Слоты компонента - концепция и практическое использование
Введение
Слоты компонента (slots) — это механизм, который позволяет передавать разметку из родительского компонента в дочерний и управлять тем, куда именно эта разметка будет вставлена внутри компонента.
Если говорить проще, слот — это «отверстие» внутри компонента, в которое родитель может положить свой HTML, другие компоненты или даже шаблоны с логикой. Эта идея встречается в нескольких технологиях:
- Web Components — через
<slot> - Vue — через
<slot>и именованные / scoped-слоты - Svelte — через
<slot>иslot="name" - Частично похожая идея в React —
props.childrenи «render props» - Angular — через
ng-content(там это называется content projection)
Чтобы не привязываться к одному конкретному фреймворку, я буду объяснять идею на уровне общей концепции, а затем показывать примеры на базе условного фреймворка, близкого по синтаксису к Vue/Web Components. Это поможет вам перенести знания куда угодно: в Vue, Svelte, Angular или даже собственную библиотеку компонентов.
Давайте разберемся, как работают слоты, какие типы слотов бывают и как их правильно использовать, чтобы компоненты оставались гибкими и понятными.
Что такое слот компонента и зачем он нужен
Компонент без слотов
Начнем с простого. Вот пример компонента «карточки», который сам контролирует всю разметку:
<!-- Card.vue -->
<template>
<div class="card">
<h2 class="card__title">Заголовок</h2>
<p class="card__content">Текст карточки</p>
<button class="card__button">Подробнее</button>
</div>
</template>
<script>
// Компонент без слотов — все части интерфейса зашиты внутри
export default {
name: "Card"
}
</script>
Такой компонент сложно переиспользовать: вы всегда получаете один и тот же заголовок, текст и кнопку. Если вам нужен другой текст или другая кнопка, придется:
- или передавать много пропсов (title, content, buttonText, icon, actions и так далее),
- или копировать и изменять компонент, что ведет к дублированию кода.
Компонент со слотом
Теперь добавим один слот:
<!-- Card.vue -->
<template>
<div class="card">
<h2 class="card__title">
<slot name="title">
<!-- Содержимое по умолчанию для слота title -->
Заголовок по умолчанию
</slot>
</h2>
<div class="card__content">
<slot>
<!-- Это дефолтный (безымянный) слот -->
Текст по умолчанию
</slot>
</div>
<div class="card__footer">
<slot name="actions">
<!-- Действия по умолчанию, если родитель не передал свои -->
<button class="card__button">Подробнее</button>
</slot>
</div>
</div>
</template>
<script>
// Компонент с тремя слотами - заголовок - контент - действия
export default {
name: "Card"
}
</script>
А вот как родительский компонент может использовать эту «карточку»:
<!-- Parent.vue -->
<template>
<Card>
<!-- Это попадет в дефолтный слот -->
<p>Описание товара</p>
<!-- Это попадет в именованный слот title -->
<template v-slot:title>
Карта товара
</template>
<!-- Это попадет в именованный слот actions -->
<template v-slot:actions>
<button class="card__button card__button--primary">Купить</button>
</template>
</Card>
</template>
<script>
// Родитель использует Card и наполняет его слотами
import Card from "./Card.vue"
export default {
components: { Card }
}
</script>
Смотрите, что изменилось:
- структура карточки осталась внутри компонента
Card; - но родитель может полностью заменить:
- заголовок,
- содержимое,
- блок действий;
- при этом, если какую-то часть родитель не передаст, сработает содержимое по умолчанию.
Это и есть основная идея слотов: компонент определяет каркас и структуру, а родитель — конкретное содержимое.
Виды слотов и как их использовать
Дефолтный слот
Дефолтный слот — это основной слот компонента. Обычно он без имени, и в разметке обозначается просто как <slot>.
Объявление дефолтного слота в компоненте
<!-- Layout.vue -->
<template>
<div class="layout">
<header class="layout__header">
Мой сайт
</header>
<main class="layout__content">
<slot>
<!-- Содержимое по умолчанию -->
Здесь пока ничего нет
</slot>
</main>
<footer class="layout__footer">
© 2026
</footer>
</div>
</template>
<script>
// Layout передает управление основным контентом родителю
export default {
name: "Layout"
}
</script>
Использование дефолтного слота
<!-- App.vue -->
<template>
<Layout>
<!-- Все, что внутри Layout - пойдет в дефолтный слот -->
<h1>Главная страница</h1>
<p>Добро пожаловать на наш сайт</p>
</Layout>
</template>
<script>
import Layout from "./Layout.vue"
export default {
components: { Layout }
}
</script>
Важно:
- если вы не передадите ничего внутрь
<Layout>...</Layout>, то внутри<slot>отобразится «Содержимое по умолчанию»; - дефолтный слот удобен для компонентов-контейнеров: макеты, модальные окна, панели.
Именованные слоты
Именованные слоты позволяют иметь несколько областей, которые родитель может независимо заполнять.
Объявление именованных слотов
<!-- PageLayout.vue -->
<template>
<div class="page-layout">
<header class="page-layout__header">
<slot name="header">
<!-- Заглушка - заголовок по умолчанию -->
Заголовок страницы
</slot>
</header>
<aside class="page-layout__sidebar">
<slot name="sidebar">
<!-- Боковая панель по умолчанию -->
Навигация
</slot>
</aside>
<main class="page-layout__main">
<slot>
<!-- Дефолтный слот для основного контента -->
Основной контент отсутствует
</slot>
</main>
</div>
</template>
<script>
// Компоновка страницы с тремя зонами - header - sidebar - main
export default {
name: "PageLayout"
}
</script>
Использование именованных слотов
<!-- SomePage.vue -->
<template>
<PageLayout>
<!-- header -->
<template v-slot:header>
<h1>Профиль пользователя</h1>
</template>
<!-- sidebar -->
<template v-slot:sidebar>
<ul>
<li>Общая информация</li>
<li>Настройки</li>
</ul>
</template>
<!-- дефолтный слот (main) -->
<p>Здесь будет основной контент профиля</p>
</PageLayout>
</template>
<script>
import PageLayout from "./PageLayout.vue"
export default {
components: { PageLayout }
}
</script>
Как видите, у вас появляется гибкая схема:
- один компонент отвечает за общую структуру страницы;
- другой — за конкретное содержимое каждой зоны;
- нет жесткой привязки к тому, что именно будет в шапке или сайдбаре.
Слоты с содержимым по умолчанию
Вы уже видели этот прием, но давайте отдельно зафиксируем, как это работает.
Содержимое между <slot>...</slot> в шаблоне дочернего компонента — это fallback-контент. Он будет показан, если:
- родитель ничего не передал в этот слот;
- или передал, но условие вывода не сработало (зависит от фреймворка).
Пример с комментарием:
<!-- Button.vue -->
<template>
<button class="button">
<slot>
<!-- Текст по умолчанию, если родитель не передал содержимое -->
Кнопка
</slot>
</button>
</template>
<script>
// Простой компонент кнопки с возможностью переопределить текст
export default {
name: "Button"
}
</script>
Использование:
<!-- 1. Используем текст по умолчанию -->
<Button />
<!-- 2. Передаем свой текст -->
<Button>Сохранить</Button>
Этот прием особенно полезен для:
- кнопок,
- иконок с подписью,
- простых блоков, где нужен понятный дефолтный текст или разметка.
Scoped slots — когда дочерний компонент «делится данными» со слотом
Иногда нужно не просто передать разметку от родителя к дочернему компоненту, а еще и дать этой разметке доступ к данным дочернего компонента. Для этого используются так называемые «scoped slots» (слоты с областью видимости).
Смотрите, как это выглядит на практике.
Проблема без scoped slots
Представьте компонент DataList, который загружает список элементов и должен отображать их как-то, но «как именно» — решает родитель.
Без scoped-слотов компонент мог бы лишь сам контролировать разметку:
<!-- DataList.vue (без scoped slots) -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - {{ item.value }}
</li>
</ul>
</template>
<script>
// Компонент сам решает как выводить каждый item
export default {
name: "DataList",
props: {
items: {
type: Array,
required: true
}
}
}
</script>
Как только вам потребуется выводить элементы в другом формате (например, таблицей или карточками), этот компонент придется модифицировать или дублировать.
Решение — scoped slots
Теперь посмотрите на версию со scoped-слотом:
<!-- DataList.vue (со scoped slot) -->
<template>
<ul>
<li
v-for="item in items"
:key="item.id"
>
<slot :item="item">
<!-- Содержимое по умолчанию, если родитель не описал слот -->
{{ item.name }} - {{ item.value }}
</slot>
</li>
</ul>
</template>
<script>
// Компонент предоставляет item во внешний слот через props слота
export default {
name: "DataList",
props: {
items: {
type: Array,
required: true
}
}
}
</script>
Здесь важный момент:
<slot :item="item">— дочерний компонент «передает» объектitemнаружу;- родитель сможет получить это значение как проп слота и использовать в своей разметке.
Теперь давайте посмотрим на использование:
<!-- Parent.vue -->
<template>
<!-- 1. Используем DataList с дефолтным выводом -->
<DataList :items="users" />
<!-- 2. Используем DataList с кастомным рендерингом -->
<DataList :items="users">
<!-- Здесь мы объявляем слот и принимаем проп item -->
<template v-slot:default="slotProps">
<!-- slotProps.item - это объект, переданный дочерним компонентом -->
<strong>{{ slotProps.item.name }}</strong>
<span>({{ slotProps.item.email }})</span>
</template>
</DataList>
</template>
<script>
// Родитель получает полную свободу в том как выводить item
import DataList from "./DataList.vue"
export default {
components: { DataList },
data() {
return {
users: [
// Массив пользователей с полями name и email
]
}
}
}
</script>
Вместо slotProps можно использовать деструктуризацию:
<DataList :items="users">
<template v-slot:default="{ item }">
<!-- Мы сразу достаем item из props слота -->
<strong>{{ item.name }}</strong>
<span>({{ item.email }})</span>
</template>
</DataList>
Теперь:
DataListуправляет только:- загрузкой или хранением массива
items, - циклом
v-for, - ключами элементов;
- загрузкой или хранением массива
- внешний компонент управляет тем, как показывать каждый элемент.
Это классический пример использования scoped-слотов.
Scoped slots с несколькими данными
Часто в слот нужно передать больше одного значения: например, сам элемент, индекс, состояние и т.д.
<!-- Table.vue -->
<template>
<table class="table">
<thead>
<tr>
<slot name="header">
<!-- Шапка по умолчанию -->
<th>#</th>
<th>Значение</th>
</slot>
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in rows"
:key="row.id"
>
<slot
name="row"
:row="row"
:index="index"
:is-even="index % 2 === 0"
>
<!-- Дефолтный вывод строки -->
<td>{{ index + 1 }}</td>
<td>{{ row.value }}</td>
</slot>
</tr>
</tbody>
</table>
</template>
<script>
// Компонент таблицы - родитель может переопределить и шапку - и строки
export default {
name: "Table",
props: {
rows: {
type: Array,
required: true
}
}
}
</script>
И использование:
<!-- ParentTable.vue -->
<template>
<Table :rows="items">
<template v-slot:header>
<th>#</th>
<th>Название</th>
<th>Статус</th>
</template>
<template v-slot:row="{ row, index, isEven }">
<tr :class="[{ 'row--even': isEven }]">
<td>{{ index + 1 }}</td>
<td>{{ row.name }}</td>
<td>{{ row.status }}</td>
</tr>
</template>
</Table>
</template>
<script>
// Родитель полностью контролирует содержимое ячеек и оформление строки
import Table from "./Table.vue"
export default {
components: { Table },
data() {
return {
items: [
// Данные для таблицы
]
}
}
}
</script>
Такой подход дает вам очень мощный инструмент для построения:
- универсальных таблиц,
- списков,
- деревьев,
- карточек со сложными внутренними состояниями.
Паттерны и практические примеры использования слотов
Паттерн «компонент-обертка» (Wrapper / Layout)
Слоты отлично подходят для компонентов, которые отвечают за общую «рамку» интерфейса: модальные окна, панели, макеты страниц.
Пример: модальное окно
<!-- Modal.vue -->
<template>
<div
v-if="visible"
class="modal-overlay"
@click.self="close"
>
<div class="modal">
<header class="modal__header">
<slot name="header">
<!-- Заголовок по умолчанию -->
Диалог
</slot>
<button
class="modal__close"
@click="close"
>
×
</button>
</header>
<section class="modal__body">
<slot>
<!-- Тело модального окна по умолчанию -->
Нет содержимого
</slot>
</section>
<footer class="modal__footer">
<slot name="footer">
<!-- Действия по умолчанию -->
<button @click="close">Закрыть</button>
</slot>
</footer>
</div>
</div>
</template>
<script>
// Компонент модального окна - поведение внутри - содержимое снаружи
export default {
name: "Modal",
props: {
visible: {
type: Boolean,
required: true
}
},
emits: ["close"],
methods: {
// Метод закрытия модального окна - оповещает родителя событием
close() {
this.$emit("close")
}
}
}
</script>
Использование:
<!-- ModalUsage.vue -->
<template>
<Modal
:visible="isOpen"
@close="isOpen = false"
>
<template v-slot:header>
Удаление элемента
</template>
<p>Вы уверены что хотите удалить этот элемент</p>
<template v-slot:footer>
<button @click="isOpen = false">Отмена</button>
<button class="btn btn--danger" @click="confirmDelete">
Удалить
</button>
</template>
</Modal>
</template>
<script>
// Родитель управляет только логикой удаления - модалка занимается отображением
import Modal from "./Modal.vue"
export default {
components: { Modal },
data() {
return {
isOpen: false
}
},
methods: {
// Метод подтверждения удаления
confirmDelete() {
// Здесь логика удаления
this.isOpen = false
}
}
}
</script>
Здесь видно четкое разделение ответственности:
Modal:- управляет видимостью,
- рисует оверлей,
- закрывается при клике по фону,
- отображает заголовок, тело, футер — но не знает, что в них.
- родитель:
- решает, что именно показывать,
- управляет флагом
isOpen, - выполняет действия по подтверждению.
Паттерн «компонент логики» (Renderless / headless component)
Еще один важный паттерн — «компонент без разметки», который отвечает только за логику и состояние, а за разметку отвечает родитель через слот.
Пример: компонент со скрытием/раскрытием
<!-- Toggle.vue -->
<template>
<!-- Компонент вообще не рисует конкретную разметку,
он только передает состояние и методы во внешний слот -->
<slot
:is-open="isOpen"
:toggle="toggle"
/>
</template>
<script>
// Toggle хранит состояние isOpen и передает его наружу
export default {
name: "Toggle",
data() {
return {
// Локальное состояние открытия
isOpen: false
}
},
methods: {
// Переключает состояние
toggle() {
this.isOpen = !this.isOpen
}
}
}
</script>
Использование:
<!-- ToggleUsage.vue -->
<template>
<Toggle v-slot="{ isOpen, toggle }">
<!-- Здесь мы сами решаем - какие элементы показывать и как -->
<button @click="toggle">
{{ isOpen ? "Скрыть" : "Показать" }} детали
</button>
<div v-if="isOpen" class="details">
<p>Какие-то подробности</p>
</div>
</Toggle>
</template>
<script>
// Родитель полностью управляет отображением на основе isOpen
import Toggle from "./Toggle.vue"
export default {
components: { Toggle }
}
</script>
Такой подход:
- выносит состояние и логику в отдельный компонент;
- позволяет переиспользовать механизм toggle в разных местах, не завязываясь на конкретные HTML-элементы.
Вы можете сделать так же:
- компонент
FetchData, который загружает данные и передает:isLoading,error,data;
- компонент
FormState, который управляет:values,errors,touched.
Все это удобно делается на scoped-слотах.
Паттерн для UI-библиотек: базовый компонент + слот для кастомизации
При создании библиотеки компонентов часто нужно дать пользователю возможность слегка кастомизировать внешний вид, не ломая основной контракт.
Например, компонент Avatar:
<!-- Avatar.vue -->
<template>
<div class="avatar">
<img
:src="src"
:alt="alt"
class="avatar__image"
/>
<slot name="badge">
<!-- Бейдж по умолчанию (опциональный) -->
</slot>
<slot name="fallback">
<!-- Фолбэк - если картинка не загрузилась, здесь можно вывести инициалы -->
</slot>
</div>
</template>
<script>
// Простая аватарка с возможностью добавить поверх бейдж или фолбэк
export default {
name: "Avatar",
props: {
src: String,
alt: String
}
}
</script>
Родитель может:
- добавить бейдж статуса;
- добавить фолбэк для ошибки загрузки.
<Avatar src="/user.png" alt="User">
<template v-slot:badge>
<span class="status status--online"></span>
</template>
<template v-slot:fallback>
<span class="avatar__initials">OM</span>
</template>
</Avatar>
Таким образом:
- базовый компонент контролирует основную структуру и классы;
- пользователь библиотеки может аккуратно менять мелкие части.
Жизненный цикл слотов и реактивность
Кто управляет данными в слоте
Важно понимать:
- разметка слота принадлежит родителю, а не дочернему компоненту;
- значит:
- выражения внутри слота (
{{ ... }}, директивы и так далее) вычисляются в контексте родительского компонента; - дочерний компонент лишь определяет, где это содержимое будет отрисовано.
- выражения внутри слота (
Посмотрим на пример:
<!-- Child.vue -->
<template>
<div class="child">
<slot />
</div>
</template>
<script>
export default {
name: "Child"
}
</script>
<!-- Parent.vue -->
<template>
<Child>
<!-- Здесь используется переменная message из Parent -->
<p>{{ message }}</p>
</Child>
</template>
<script>
import Child from "./Child.vue"
export default {
components: { Child },
data() {
return {
// Это состояние принадлежит родителю
message: "Привет"
}
}
}
</script>
Если message изменится в родителе:
- фреймворк заново отрендерит слот;
- дочерний компонент автоматически покажет обновленное значение.
Реактивность в scoped slots
В scoped-слотах реактивность работает в обе стороны:
- данные слота (например
item) принадлежат дочернему компоненту; - выражения, которые используют эти данные, принадлежат родителю.
<!-- ChildList.vue -->
<template>
<slot
v-for="item in items"
:key="item.id"
:item="item"
/>
</template>
<script>
export default {
name: "ChildList",
props: {
items: Array
}
}
</script>
<!-- Parent.vue -->
<template>
<ChildList :items="items" v-slot="{ item }">
<p>{{ item.text }}</p>
</ChildList>
</template>
<script>
import ChildList from "./ChildList.vue"
export default {
components: { ChildList },
data() {
return {
items: [
// Массив объектов с полем text
]
}
},
mounted() {
// Если вы через время измените items -
// список перерендерится - и слот тоже
}
}
</script>
Если items изменятся:
- дочерний компонент создаст новый набор слотов;
- родительская разметка обновится, используя новые значения
item.
Лучшие практики при работе со слотами
Где использовать слоты, а где пропсы
Слоты хорошо подходят, когда нужно передать:
- фрагмент разметки;
- компонент или несколько компонентов;
- участок UI, который невозможно описать только данными.
Пропсы хорошо подходят, когда нужно передать:
- данные (строки, числа, объекты);
- флаги (например
disabled,loading); - конфигурацию (например
type="primary").
Обычно:
- если вы передаете данные — используйте пропсы;
- если вы передаете вид (разметку, компоненты) — используйте слоты;
- если нужно добавить кастомное поведение к стандартному виду — комбинируйте: пропсы + слоты.
Не переусердствуйте с количеством слотов
Иногда возникает соблазн добавить:
- слот
icon-left, - слот
icon-right, - слот
before-text, - слот
after-text, - слот
wrapperи т.д.
В результате:
- компонент становится трудно понимать;
- никак не ясно, какие комбинации слотов допустимы.
Рекомендация:
- выделяйте слоты под логические блоки (header, footer, body, sidebar, actions);
- не дробите их слишком мелко;
- если конфигурация становится слишком сложной, возможно, компонент делает слишком много.
Документируйте API слотов
При описании компонента указывайте:
- какие слоты есть;
- какие у них имена;
- какое содержимое по умолчанию;
- какие props доступны в scoped-слотах.
Пример короткой документации:
default— основной контент модального окна;header— заголовок модального окна (по умолчанию «Диалог»);footer— нижняя панель с кнопками (по умолчанию «Закрыть»);row(scoped) — слот строки таблицы:- props:
row,index,isEven.
- props:
Это помогает другим разработчикам быстрее понять, как использовать компонент.
Держите логику внутреннего состояния внутри компонента
Слоты не должны «разрывать» принцип инкапсуляции:
- внутренние детали реализации компонента не должны вытекать наружу без необходимости;
- scoped-слоты должны отдавать только те данные, которые действительно нужны для настройки отображения.
Если вы чувствуете, что вынуждены отдавать наружу слишком много деталей (например, внутреннюю структуру стейта), проверьте, не смешали ли вы в одном компоненте слишком много ответственности.
Распространенные ошибки и нюансы
Ошибка: использование переменных дочернего компонента в слоте без scoped-слотов
Многие разработчики пытаются сделать так:
<!-- BadChild.vue -->
<template>
<div>
<slot />
</div>
</template>
<script>
export default {
name: "BadChild",
data() {
return {
// Локальное состояние которое нельзя использовать напрямую в слоте
count: 0
}
}
}
</script>
<!-- BadParent.vue -->
<template>
<BadChild>
<!-- Ошибка - здесь count недоступен, он принадлежит дочернему компоненту -->
<p>Счетчик - {{ count }}</p>
</BadChild>
</template>
Так делать нельзя. Родитель не видит count, потому что он хранится в дочернем компоненте. Чтобы передать count наружу, нужно использовать scoped-слот:
<!-- GoodChild.vue -->
<template>
<div>
<slot :count="count" />
</div>
</template>
И уже потом:
<!-- GoodParent.vue -->
<template>
<GoodChild v-slot="{ count }">
<p>Счетчик - {{ count }}</p>
</GoodChild>
</template>
Ошибка: логика в слоте, завязанная на внутренние детали компонента
Иногда внешний код начинает полагаться на то, как именно компонент рендерит слоты внутри. Например:
- считать, что слот
rowвсегда будет внутри<tr>; - или рассчитывать на конкретный порядок рендеринга.
Старайтесь не использовать внутреннюю структуру компонента слишком жестко. Если вам нужна гарантия, что слот будет отрисован, например, в <tr>, лучше:
- описать это в документации;
- или явно передать нужную обертку через слот (то есть не оборачивать слот жестко внутри тега).
Ошибка: слишком сложные выражения в слотах
Поскольку слоты часто используются для кастомизации интерфейса, иногда внутрь слота начинают добавлять много логики: условные рендеры, вычисления, вложенные циклы.
Это:
- ухудшает читаемость;
- делает разметку громоздкой;
- усложняет отладку.
Лучше:
- выносить сложную логику в методы или вычисляемые свойства родителя;
- использовать в слоте уже подготовленные данные.
Пример:
<!-- Вместо сложных выражений прямо в слоте -->
<template v-slot:row="{ row }">
<td>{{ formatDate(row.createdAt) }}</td>
<td>{{ statusLabel(row.status) }}</td>
</template>
Где formatDate и statusLabel — методы или computed в родительском компоненте.
Заключение
Слоты компонента — это фундаментальный механизм для построения гибких и переиспользуемых UI-компонентов. Они позволяют:
- разделять структуру и содержимое;
- передавать разметку от родителя к дочернему компоненту;
- делить интерфейс на логические зоны через именованные слоты;
- делиться данными из дочернего компонента с помощью scoped-слотов;
- строить «логические» (renderless/headless) компоненты, которые отвечают только за состояние и поведение.
Если резюмировать подход:
- используйте дефолтный слот для основного содержимого;
- добавляйте именованные слоты для зон, которые логически отличаются (header, footer, actions);
- применяйте scoped-слоты, когда дочерний компонент должен предоставить наружу свои данные, но не хочет жестко задавать разметку;
- сочетайте пропсы и слоты, чтобы разделить данные и вид.
На практике слоты помогают строить библиотеки компонентов, создавать гибкие макеты, таблицы, списки и сложные UI-паттерны без копипаста и жестко зашитой разметки.
Частозадаваемые технические вопросы
1. Как типизировать props слотов при использовании TypeScript
В большинстве современных фреймворков (например, Vue с TypeScript) есть специальные механизмы для описания типов props слотов. Общая идея:
- описать интерфейс пропсов слота;
- использовать его в определении компонента.
Пример для Vue с Composition API (схематично):
// Описываем интерфейс пропсов слота row
interface RowSlotProps {
row: Row
index: number
}
// В defineComponent указываем тип слотов
export default defineComponent({
// ...
setup(props, { slots }) {
// slots.row - (props - RowSlotProps) => VNode[]
}
})
На стороне родителя редактор подскажет поля row, index и их типы внутри scoped-слота.
2. Как протестировать содержимое слотов в unit-тестах
В большинстве тестовых утилит для компонентов (например, Vue Test Utils):
- При монтировании компонента можно передать слоты:
mount(Component, {
slots: {
default: "<div>Тест</div>",
header: "<h1>Заголовок</h1>"
}
})
- Затем вы проверяете:
- наличие нужных элементов;
- текст внутри них;
- реакцию на события, если слот содержит интерактивный элемент.
Для scoped-слотов можно передавать функцию, которая получает props и возвращает разметку. В тестах вы можете отрендерить компонент и убедиться, что данные из props слота корректно используются в шаблоне.
3. Можно ли вложить один слот в другой и как это работает
Слоты могут быть вложенными, но важно учитывать:
- каждый слот принадлежит своему компоненту;
- если вы передаете в один слот компонент, который сам имеет свои слоты, то заполнять его слоты нужно уже на уровне того компонента.
Например:
Parentпередает содержимое в слотCard;Cardвнутри имеет слотactions;- чтобы заполнить
actions, нужно использовать<template v-slot:actions>именно внутри использованияCard, а неParent.
Главное — правильно понимать, чей это слот и на каком уровне он объявлен.
4. Как динамически выбирать слот в зависимости от условия
Для динамического выбора слота обычно используют условный рендеринг на стороне родителя. Например:
<Component>
<template v-if="isAdmin" v-slot:header>
Админ панель
</template>
<template v-else v-slot:header>
Пользовательская панель
</template>
</Component>
То есть:
- один и тот же именованный слот
header; - но содержимое выбирается по условию;
- фреймворк сам позаботится о корректном обновлении при изменении
isAdmin.
5. Как оптимизировать производительность при большом количестве scoped-слотов
Scoped-слоты могут быть дорогими, если:
- у вас много элементов (например, длинный список);
- каждый элемент рендерит сложный scoped-слот.
Подходы к оптимизации:
- Виртуализация списка — рендерить только видимую часть элементов (через специальные компоненты вроде виртуального списка).
- Мемоизация тяжелых вычислений в родительском компоненте — выносить трудоемкие операции в вычисляемые свойства или кешируемые функции.
- Упрощение разметки в слоте — по возможности не делать там лишних оберток и тяжелых структур.
- Разделение на более простые компоненты — если слот стал слишком сложным, вынесите его в отдельный подкомпонент, чтобы уменьшить количество вычислений при каждом обновлении.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев