Compound components

Compound Components — это паттерн проектирования интерфейсов, который позволяет создавать сложные компоненты из нескольких взаимосвязанных подкомпонентов, сохраняя при этом инкапсуляцию состояния и гибкость API. В SvelteKit этот подход особенно удобен благодаря реактивной системе, контексту и слотам.

Основная идея заключается в том, что родительский компонент управляет состоянием, а дочерние компоненты получают доступ к этому состоянию через context API или через пропсы, не требуя явной передачи данных на каждом уровне вложенности.


Использование слотов для связи компонентов

SvelteKit предоставляет слоты, которые идеально подходят для реализации compound components. Родительский компонент может определять основной слот, а дочерние компоненты используют этот слот для отображения своих частей. Пример структуры:

<!-- Tabs.svelte -->
<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const activeTab = writable(0);
  setContext('tabs', { activeTab });
</script>

<div class="tabs">
  <slot></slot>
</div>
<!-- TabList.svelte -->
<script>
  import { getContext } from 'svelte';
  const { activeTab } = getContext('tabs');
</script>

<div class="tab-list">
  <slot></slot>
</div>
<!-- Tab.svelte -->
<script>
  import { getContext } from 'svelte';
  export let index;
  const { activeTab } = getContext('tabs');
</script>

<button class:active={$activeTab === index} on:click={() => activeTab.set(index)}>
  <slot></slot>
</button>

<style>
  .active {
    font-weight: bold;
  }
</style>
<!-- Использование -->
<Tabs>
  <TabList>
    <Tab index={0}>Вкладка 1</Tab>
    <Tab index={1}>Вкладка 2</Tab>
  </TabList>
</Tabs>

В этом примере Tabs управляет состоянием, TabList просто рендерит контейнер для кнопок, а каждый Tab использует контекст для определения активности.


Контекст как ключевой инструмент

Использование context API в Svelte позволяет компонентам делиться состоянием без необходимости прокидывать пропсы через каждый уровень. Родитель задает setContext, дочерние получают getContext. Это делает compound components более читаемыми и масштабируемыми.

Преимущества контекста:

  • Не требует лишних пропсов.
  • Позволяет поддерживать связь между компонентами, которые не находятся на прямой иерархии.
  • Обеспечивает реактивность через writable или readable store.

Инкапсуляция состояния и событий

Compound components обычно требуют управления внутренним состоянием и событий. В SvelteKit для этого используются:

  • Writable store для реактивного состояния.
  • Custom events для оповещения родителей о действиях дочерних компонентов.

Пример с Accordion:

<!-- Accordion.svelte -->
<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const openIndex = writable(null);
  setContext('accordion', { openIndex });
</script>

<div class="accordion">
  <slot></slot>
</div>
<!-- AccordionItem.svelte -->
<script>
  import { getContext, createEventDispatcher } from 'svelte';
  export let index;
  const { openIndex } = getContext('accordion');
  const dispatch = createEventDispatcher();

  function toggle() {
    openIndex.update(i => i === index ? null : index);
    dispatch('toggle', { index });
  }
</script>

<div class="item">
  <div class="header" on:click={toggle}>
    <slot name="header"></slot>
  </div>
  {#if $openIndex === index}
    <div class="content">
      <slot name="content"></slot>
    </div>
  {/if}
</div>

Паттерн “Compound components + slots + context”

Сочетание этих трех инструментов позволяет:

  1. Создавать гибкие UI-компоненты, которые легко кастомизируются.
  2. Поддерживать разделение логики и визуальной структуры.
  3. Реализовывать реактивные состояния без лишнего дублирования кода.

Пример итогового паттерна: Tabs, Accordion, Dropdown, Modal. Все они имеют родительский компонент, управляющий состоянием, и дочерние компоненты, получающие контекст и реагирующие на него.


Рекомендации по проектированию

  • Использовать один контекст на группу компонентов, чтобы избежать лишних store.
  • Дочерние компоненты должны быть прозрачными и не зависеть от внешнего состояния кроме контекста.
  • Для сложных состояний использовать derived store для вычисляемых значений.
  • Разделять UI и логику: родитель отвечает за состояние, дочерние — за отображение и события.

Примеры расширения функциональности

  1. Lazy rendering — отображение дочернего компонента только при активации.
  2. Compound components с динамическим списком — генерируются через #each.
  3. Комбинирование нескольких паттернов — например, Tabs внутри Modal с отдельным контекстом.

Такая структура позволяет создавать сложные интерфейсы без потери реактивности и управляемости, сохраняя чистоту кода и гибкость для расширения.