ハイドレーションまでの空白期間
ストリーミングSSRでは、初回描画からハイドレーションの完了までに空白期間があり、Suspenseでは遅れて届く境界ごとにその期間が生じます。その間、useStateのアコーディオンは表示されても操作できませんが、<details>のアコーディオンは初回描画から動作します。
同じアコーディオンを、<script>タグを一切使わずにレンダリングした2つのドキュメントがあります。`<details>`を使うものと`useState`を使うものを開くと、前者は開き、後者は開きません。ソースでも確認できます。
目標は、不要なレンダリング、プロバイダー全体への更新の波及、ライブラリのJavaScriptによる同期レイアウト処理をそれぞれゼロにすることです。状態はどこかで描画する必要があるため、レンダリング自体をゼロにするわけではありません。JavaScriptは標準でゼロとし、アクセシビリティに必要な部分にだけ最小限使います。
Tier
各コンポーネントは、ビルドが読み込むマニフェストにTierを宣言します。このマニフェストが、サイズ予算、RSCの純粋性を検査するlint、Playwrightのテストスイート、各コンポーネントページのTierバッジを決定します。
| Tier | 予算 | JavaScriptが無効な場合 | 意味 |
|---|---|---|---|
| Tier 0 RSC — クライアントJavaScriptゼロ | 0 kB | JavaScript無効のテストスイートをすべて通過。 | 状態はネイティブ要素が保持するか、そもそも状態を持ちません。useMemoやuseRefも含め、すべてのReactフックを禁止します。 |
| Tier 1 RSCシェルと小さなクライアントアイランド | ≤ 1 kB | 機能を制限し、コンポーネントごとに明記。 | 状態は引き続きネイティブ要素が保持します。JavaScriptはARIAの同期や、ホバー意図の判定、自動終了などプラットフォームにない機能の追加にのみ使います。 |
| Tier 2 クライアントコンポーネント — ハイドレーション前のSSR | コンポーネントごと | ハイドレーション前に意味のあるマークアップを描画。 | クライアントエントリーは1つです。サーバーは可能な限りネイティブ優先のマークアップを出力します。キーボードナビゲーション、フィルタリング、ARIAの同期はハイドレーション後に動作します。 |
Tierは移行にも使えます。判定条件をコンポーネント単位で有効にし、1つずつアーキテクチャを厳格にできます。
レイヤー構成
@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の型だけをインポートできます。
elementsを公開レイヤーにする理由
多くのアプリは@gunkin/uiify/elementsを直接インポートしません。公開する理由は2つあります。
1つ目は、設計を検証できることです。コンポーネントページに*elements/dialogを土台に構築*とあれば、要素のページで内部の正確なマークアップとdata-*契約を確認できます。
2つ目は、組み合わせて使えることです。コンポーネントの構造がデザインに合わなければ、仕組みを作り直さず、同じ接続を使って独自の構造を組み立てられます。
このレイヤーに加わるモジュールは、5つの条件をすべて満たす必要があります。ネイティブ機構が1つであること、JavaScriptによる動作を持たないこと、@gunkin/uiify/componentsに利用箇所が2つ以上あること、CSSセレクターで状態を読めること、間接参照を増やすのではなくマークアップの重複を減らすことです。
モジュール名はUIの概念ではなく、details、dialog、popover、invokerなどプラットフォームの機能に由来します。コンポーネントと要素は同名にならないため、Modalはdialogを土台にしています。要素を見る
Tier 0でReact contextを禁止する理由
パーツ間の通信がReact contextに依存したままでは、状態をDOMに移しても意味がありません。Server Componentはcontextを使えないため、プロバイダーを共有するRootとItemは、マークアップがどれほどネイティブでもサーバーでレンダリングできません。
接続はシリアライズ後も維持する必要があるため、パーツはHTMLでつなぎます。グループ化には<details name>、ブラウザーが解決する参照にはcommandforとpopovertarget、IDにはpropsから導出した値を使います。useIdの値はcontextなしでは兄弟パーツに渡せません。
<Accordion id="faq">
<Accordion.Item value="shipping"> {/* id="faq-shipping" */}代わりに、ルートにはidが必要です。組み合わせが不要なら、コンポーネントがデータを受け取り、ツリー全体をレンダリングします。Server Componentを見る