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 для этого используются:
Пример с 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>
Сочетание этих трех инструментов позволяет:
Пример итогового паттерна: Tabs, Accordion, Dropdown, Modal. Все они имеют родительский компонент, управляющий состоянием, и дочерние компоненты, получающие контекст и реагирующие на него.
#each.Такая структура позволяет создавать сложные интерфейсы без потери реактивности и управляемости, сохраняя чистоту кода и гибкость для расширения.