Svelte
Паттерны компонентов
Команде нужен компонент таблицы данных: сортировка, выбор строк, пагинация. Но дизайн у каждого экрана свой, и навязать единую разметку нельзя. Если зашить разметку в компонент, его придётся форкать под каждый случай. Если вынести только логику, а отрисовку отдать наружу, один компонент покроет все экраны. Этот сдвиг от компонента-с-разметкой к компоненту-с-логикой и есть суть headless-паттерна, а связывает их в Svelte механизм сниппетов.
- Headless UI-библиотеки: Melt UI и Bits UI дают логику и доступность без навязанных стилей
- Таблицы данных: один движок сортировки и пагинации, а разметку ячеек задаёт потребитель
- Compound-компоненты: Accordion и его Item делятся состоянием раскрытия через контекст
- Комбобоксы и автодополнение: вся логика навигации с клавиатуры в headless-компоненте, вид задаёт приложение
Предварительные знания
- Понимание сниппетов: объявление через #snippet и вызов через @render
- Знание пропсов и того, как передавать сниппет в компонент как проп
- Базовое понимание контекста для compound-компонентов
Сниппеты как render-props
Render-prop это приём, при котором компонент не решает, как отрисовать элемент, а просит об этом потребителя, передавая ему данные. В Svelte 5 эту роль играют сниппеты. Дочерний компонент принимает сниппет пропсом и вызывает его через @render, передавая в него значения. Потребитель определяет сниппет и решает, как именно показать эти значения. Так компонент управляет данными, а разметку оставляет вызывающей стороне.
List не знает, как выглядит строка: он лишь перебирает items и для каждого вызывает row(item). Потребитель получает item обратно в сниппет и оформляет его по своему вкусу. Один и тот же List переиспользуется для пользователей, товаров, заказов с разной разметкой строки. Это инверсия управления отрисовкой: данные сверху вниз, оформление снизу вверх.
Сниппет, переданный между открывающим и закрывающим тегом компонента, становится его пропсом по имени. Сниппет с именем children доступен как проп children и рендерится через @render children(). Это прямой аналог дочернего контента в других фреймворках, но с возможностью передавать параметры.
Компонент List перебирает items и для каждого вызывает {@render row(item)}. Что это даёт потребителю по сравнению с зашитой внутри List разметкой строки?
Headless и renderless компоненты
Headless-компонент инкапсулирует логику и состояние, но не диктует внешний вид. Он содержит поведение (например, переключение раскрытия, навигацию с клавиатуры, управление выбором) и выдаёт наружу данные и обработчики, а как это отрисовать, решает потребитель через сниппет. Renderless-компонент идёт дальше: он вообще не рендерит собственного DOM, а только предоставляет состояние и поведение, целиком отдавая разметку наружу.
Toggle хранит состояние on и метод toggle, но не рендерит ни одной кнопки сам. Он передаёт { on, toggle } в сниппет children, и потребитель строит любой интерфейс поверх этой логики: кнопку, переключатель, чекбокс. Одна и та же логика переключения переиспользуется с произвольной разметкой, потому что компонент не привязан к конкретному виду.
- Headless-компонент — Инкапсулирует логику и состояние, может рендерить минимальную структурную обёртку, но вид элементов отдаёт наружу через сниппеты
- Renderless-компонент — Не рендерит собственного DOM вовсе, только прокидывает состояние и поведение в сниппет. Максимум гибкости разметки
Headless-подход стоит за такими библиотеками, как Melt UI и Bits UI: они дают доступную, проверенную логику виджетов (диалоги, меню, комбобоксы) без единого навязанного стиля. Команда получает корректное поведение и сама отвечает за внешний вид.
Чем renderless-компонент отличается от обычного компонента с зашитой разметкой?
Паттерн prop-getter
Когда headless-компонент отдаёт поведение наружу, потребителю нужно навесить на свой элемент правильные обработчики и атрибуты: onclick, aria-атрибуты, состояние. Перечислять их вручную на каждом использовании утомительно и легко ошибиться. Паттерн prop-getter решает это: компонент отдаёт функцию, которая возвращает готовый объект пропсов, а потребитель разворачивает его в свой элемент одним spread.
getButtonProps() возвращает объект с onclick и aria-expanded, и потребитель применяет его через {...getButtonProps()}. Вся логика раскрытия и корректные aria-атрибуты доступности приходят одним разворотом. Потребителю не нужно помнить, какие именно атрибуты обязательны: компонент собирает их сам. Это снижает связанность и убирает класс ошибок забытых обработчиков и атрибутов.
| Паттерн | Что отдаёт наружу | Что делает потребитель |
|---|---|---|
| Render-prop через сниппет | Данные в сниппет | Определяет разметку для данных |
| Headless-компонент | Состояние и поведение | Строит вид поверх логики |
| Prop-getter | Функцию, возвращающую пропсы элемента | Разворачивает пропсы в свой элемент через spread |
Prop-getter хорошо сочетается с возможностью переопределения: getter может принять пользовательские пропсы и аккуратно слить их со своими, например объединив обработчики кликов. Так потребитель добавляет своё поведение, не теряя того, что обеспечивает компонент.
В чём преимущество prop-getter перед тем, чтобы потребитель вручную навешивал onclick и aria-expanded на свой элемент?
Связь с другими темами
Паттерны компонентов строятся на сниппетах и контексте, а раскрываются с типами:
- Сниппеты — Сниппет как render-prop это механизм, на котором держатся гибкие паттерны
- Context API — Compound-компоненты делятся состоянием между родителем и потомками через контекст
- TypeScript в Svelte — Типизация сниппетов и дженерики делают эти паттерны безопасными в использовании
Итог
- Композиция собирает интерфейс из мелких компонентов, а сниппеты позволяют родителю передавать куски разметки внутрь дочернего
- Сниппет как render-prop это параметризованный фрагмент: дочерний компонент вызывает его, передавая данные обратно наверх
- Headless-компонент инкапсулирует логику и состояние, но не навязывает разметку, отдавая отрисовку потребителю
- Renderless-компонент не рендерит собственного DOM вовсе, а только предоставляет данные и поведение через сниппет
- Паттерн prop-getter выдаёт готовый набор пропсов для элемента, чтобы потребитель навесил поведение одним разворотом
Связанные уроки
- sv-12-snippets — Сниппеты как render-props это фундамент гибких паттернов, поэтому понимание сниппетов обязательно
- sv-31-context-api — Compound-компоненты и headless-логика часто делятся состоянием через контекст
- sv-36-typescript-svelte — Эти паттерны раскрываются полностью с типизацией: типы сниппетов и дженерики делают API безопасным