К содержимому
uiify

Архитектура

В uiify пять слоёв: каждый публикуется отдельно и никогда не импортирует слой выше себя. Их объединяет одно правило: компонент должен работать до гидратации.

Пауза до гидратации

При потоковом SSR между первой отрисовкой и завершением гидратации возникает пауза, а под Suspense каждая запоздавшая граница имеет свою. В эту паузу аккордеон на useState уже виден, но ещё не работает, а аккордеон на <details> работает с первой отрисовки.

Два документа отображают один и тот же аккордеон без единого тега <script>: первый на `<details>` и второй на `useState`. Откройте оба: первый раскрывается, второй — нет. Это можно проверить в исходном коде страницы.

Цель — исключить лишние рендеринги, обновления всех потребителей провайдера и синхронные расчёты компоновки из JavaScript библиотеки. Полностью исключить рендеринги нельзя: состояние должно где-то отображаться. По умолчанию JavaScript отсутствует, а там, где его требует доступность, он минимален.

Tier

Для каждого компонента в манифесте указан Tier, который считывается при сборке. Манифест определяет лимит размера, линт чистоты RSC, набор тестов Playwright и значок Tier на странице компонента.

TierЛимитБез JavaScriptЧто это значит
Tier 0
RSC — без клиентского JavaScript
0 кБПолностью проходит тесты без JavaScript.Состояние хранится в нативном элементе либо отсутствует. Запрещены все хуки React, включая useMemo и useRef.
Tier 1
Оболочка RSC + небольшой клиентский остров
≤ 1 кБОграниченное поведение, описанное для каждого компонента.Состоянием по-прежнему владеет нативный элемент. JavaScript лишь синхронизирует ARIA или добавляет то, чего нет в платформе, например задержку при наведении или автоматическое закрытие.
Tier 2
Клиентский компонент — SSR до гидратации
для каждого компонентаФормирует содержательную разметку до гидратации.Одна клиентская точка входа. Сервер по возможности формирует разметку на основе нативных механизмов; навигация с клавиатуры, фильтрация и синхронизация ARIA ждут гидратации.

Tier также помогают миграции: проверки включаются для каждого компонента отдельно, поэтому архитектурные требования ужесточаются постепенно.

Стек слоёв

@gunkin/uiify/hooks       SSR-safe React hooks — the substrate
@gunkin/uiify/core        headless behaviour: focus scope, dismiss layers, anchoring
@gunkin/uiify/elements    native-mechanism wrappers — zero client JavaScript
@gunkin/uiify/components  unstyled components, RSC by default
@gunkin/uiify/styles      optional CSS design layer

Нижние слои никогда не импортируют верхние. У @gunkin/uiify/elements самые строгие ограничения: он может импортировать только @gunkin/uiify/core/render и типы React.

Зачем элементы вынесены в публичный слой

Большинство приложений не импортируют @gunkin/uiify/elements напрямую. Этот слой публикуется по двум причинам.

Во-первых, он позволяет проверять устройство библиотеки. Если страница компонента сообщает, что он _построен на elements/dialog_, на странице элемента показаны точная разметка и контракт data-*, на которых он основан.

Во-вторых, ради композиции: если анатомия компонента не подходит вашему дизайну, создайте собственную на той же основе, не реализуя механизм заново.

Модуль попадает в слой, только если выполняет все пять правил: один нативный механизм, отсутствие поведения на JavaScript, два или более потребителя в @gunkin/uiify/components, состояние, читаемое CSS-селектором, и сокращение дублирования разметки без лишних уровней косвенности.

Модули названы по возможностям платформы — details, dialog, popover, invoker, — а не по понятиям интерфейса. Имена компонента и элемента никогда не совпадают, поэтому Modal построен на dialog. К элементам

Почему контекст React запрещён в Tier 0

Перенос состояния в DOM ничего не даёт, если части по-прежнему общаются через контекст React. Server Components не могут использовать контекст, поэтому Root и Item с общим провайдером нельзя отобразить на сервере, какой бы нативной ни была их разметка.

Связи должны переживать сериализацию, поэтому части соединяются через HTML: <details name> для группировки, commandfor и popovertarget для ссылок, которые разрешает браузер, и идентификаторы, полученные из свойств. Значение useId не может попасть в соседние части без контекста.

<Accordion id="faq">
  <Accordion.Item value="shipping">   {/* id="faq-shipping" */}

Цена такого подхода: корню нужен id. Если композиция не нужна, компонент принимает данные и сам формирует дерево. См. Server Component