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 безопасным
Паттерны компонентов

0

1

Войти