Пауза до гидратации
При потоковом 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