O intervalo até a hidratação
O SSR com streaming deixa um intervalo entre a primeira renderização na tela e o fim da hidratação. Com Suspense, cada fronteira que chega depois tem seu próprio intervalo. Nesse período, um acordeão com useState fica visível, mas inerte; um com <details> funciona desde a primeira renderização na tela.
Dois documentos renderizam o mesmo acordeão sem nenhuma tag <script>: um com `<details>` e outro com `useState`. Abra os dois: o primeiro abre; o segundo, não. Confira o código-fonte para confirmar.
A meta é eliminar renderizações desnecessárias, atualizações que se propagam por todo o provedor e trabalho síncrono de layout causado pelo JavaScript da biblioteca — não eliminar toda renderização, já que o estado precisa ser renderizado em algum lugar. Por padrão, não há JavaScript; ele é mínimo e usado apenas onde a acessibilidade exige.
Tiers
Cada componente declara seu tier em um manifesto lido pelo build. O manifesto define o limite de tamanho, o lint de pureza RSC, a suíte Playwright e o selo de tier em cada página de componente.
| Tier | Limite | Sem JavaScript | O que significa |
|---|---|---|---|
| Tier 0 RSC — sem JavaScript no cliente | 0 kB | Passa integralmente na suíte sem JavaScript. | O estado fica em um elemento nativo, ou não há estado. Todos os hooks React são proibidos, inclusive useMemo e useRef. |
| Tier 1 Estrutura RSC + pequena ilha no cliente | ≤ 1 kB | Comportamento reduzido, documentado por componente. | O elemento nativo continua sendo responsável pelo estado. O JavaScript apenas sincroniza ARIA ou acrescenta o que falta à plataforma, como intenção de passar o ponteiro ou fechamento automático. |
| Tier 2 Componente no cliente — SSR antes da hidratação | por componente | Renderiza marcação útil antes da hidratação. | Uma única entrada de cliente. O servidor emite marcação baseada em elementos nativos sempre que possível; navegação por teclado, filtragem e sincronização de ARIA aguardam a hidratação. |
Os tiers também ajudam na migração: os critérios são ativados por componente, tornando a arquitetura mais rigorosa um componente por vez.
A pilha de camadas
@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 layerCamadas inferiores nunca importam as superiores. @gunkin/uiify/elements é a mais restrita: só pode importar @gunkin/uiify/core/render e tipos do React.
Por que elements é uma camada pública
A maioria das aplicações nunca importa @gunkin/uiify/elements diretamente. A camada é publicada por dois motivos.
Primeiro, ela permite verificar o projeto. Quando uma página de componente diz _construído sobre elements/dialog_, a página desse elemento mostra a marcação exata e o contrato data-* que o sustentam.
Segundo, a composição: se a anatomia do componente não atende ao seu design, construa a sua sobre as mesmas conexões, em vez de reimplementar o mecanismo.
Um módulo só entra na camada se cumprir as cinco regras: um mecanismo nativo, nenhum comportamento em JavaScript, dois ou mais consumidores em @gunkin/uiify/components, estado legível por um seletor CSS e menos marcação duplicada, em vez de mais indireção.
Os módulos recebem nomes de recursos da plataforma — details, dialog, popover, invoker —, não de conceitos de UI. Um componente nunca tem o mesmo nome de um elemento; por isso, Modal é construído sobre dialog. Explore os elementos
Por que o contexto do React é proibido no Tier 0
Mover o estado para o DOM não adianta se as partes ainda se comunicam pelo contexto do React. Server Components não podem usar contexto; portanto, um Root e um Item que compartilham um provedor não podem ser renderizados no servidor, por mais nativa que seja sua marcação.
As conexões precisam sobreviver à serialização. Por isso, as partes se conectam por HTML: <details name> para agrupamento, commandfor e popovertarget para referências resolvidas pelo navegador e ids derivados de props. Um valor de useId não chega às partes irmãs sem contexto.
<Accordion id="faq">
<Accordion.Item value="shipping"> {/* id="faq-shipping" */}O custo: a raiz precisa de um id. Quando a composição não é necessária, o componente recebe dados e renderiza a árvore por conta própria. Veja Server Components