Слоты компонента - концепция и практическое использование

28 января 2026
Автор

Олег Марков

Введение

Слоты компонента (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.

Это помогает другим разработчикам быстрее понять, как использовать компонент.

Держите логику внутреннего состояния внутри компонента

Слоты не должны «разрывать» принцип инкапсуляции:

  • внутренние детали реализации компонента не должны вытекать наружу без необходимости;
  • 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):

  1. При монтировании компонента можно передать слоты:
mount(Component, {
  slots: {
    default: "<div>Тест</div>",
    header: "<h1>Заголовок</h1>"
  }
})
  1. Затем вы проверяете:
  • наличие нужных элементов;
  • текст внутри них;
  • реакцию на события, если слот содержит интерактивный элемент.

Для 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-слот.

Подходы к оптимизации:

  1. Виртуализация списка — рендерить только видимую часть элементов (через специальные компоненты вроде виртуального списка).
  2. Мемоизация тяжелых вычислений в родительском компоненте — выносить трудоемкие операции в вычисляемые свойства или кешируемые функции.
  3. Упрощение разметки в слоте — по возможности не делать там лишних оберток и тяжелых структур.
  4. Разделение на более простые компоненты — если слот стал слишком сложным, вынесите его в отдельный подкомпонент, чтобы уменьшить количество вычислений при каждом обновлении.
Регистрация компонентов component-registration в приложениях с внедрением зависимостейСтрелочка вправо

Постройте личный план изучения Vue до уровня Middle — бесплатно!

Vue — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Vuex - полное руководство по управлению состоянием во Vue приложенияхРеактивные ссылки ref - полный разбор для разработчиковРеактивные объекты reactive-objects - подробное руководство с примерамиРеактивные переменные - концепция reactive и практические примерыМеханизм Provide Inject - как он работает и когда применятьPinia современный менеджер состояния для VueЛокальное состояние local state в веб разработкеГлобальное состояние в приложениях - global state
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
Функция append в Go GolangОтображение компонента mounted - практическое руководствоХуки жизненного цикла компонентов - полное руководство для разработчиковУничтожение компонента destroyed - как правильно очищать ресурсы и подпискиИнициализация данных в состоянии created - как и когда подготавливать данные в приложенииОбновление компонента beforeUpdate во VueМонтирование компонента - хук beforeMount в VueРазрушение компонента во Vue - beforeDestroy и beforeUnmountСоздание экземпляра beforeCreate - полный разбор жизненного цикла
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Слоты компонента - концепция и практическое использованиеРегистрация компонентов component-registration в приложениях с внедрением зависимостейProps компонента в React - полный разбор с примерамиФункциональные компоненты в React - функциональный подход к построению интерфейсовСобытия компонента - events в современных интерфейсахДинамические компоненты - dynamic-componentsСоздание компонента component - практическое руководствоАсинхронные компоненты async-components - практическое руководство
Наблюдатели watchers - от паттерна до практических реализацийУправление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsШаблоны Vue templates - практическое руководство для разработчиковИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueРеактивность Vue reactivity - как это работает под капотом и как этим пользоватьсяПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueОпции компонента в Go - паттерн component-optionsРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreВычисляемые свойства computed во Vue.jsСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueОсновы Vue - vue-basics для уверенного стартаИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueТелепортация - архитектура и реализация в серверных приложенияхРабота с teleport для управления DOM во VueSuspense в React - управление асинхронными данными и ленивой загрузкойПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиПлагины Vue vue-plugins - полное практическое руководствоРуководство по nextTick для работы с DOMМиксины - mixins в современном программированииJSX в Vue с использованием плагина vue-jsxСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьКастомные элементы - Custom Elements в современном JavaScriptИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Vue 3 и Pinia

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий