Island
登録済みコンポーネント 1 つを、検証される JSON props 境界を持つクライアントの島としてラップします。
<Island> ラッパー
zfb または @takazudo/zfb から Island をインポートし、ブラウザーで動かすコンポーネント 1 つをラップします。ページとツリーの残りは通常の HTML としてレンダリングされます。以下の例では短い zfb 形式を使います。
import { Island } from "zfb";
import Counter from "./Counter";
export default function Page() {
return (
<main>
<h1>My Page</h1>
<Island when="visible">
<Counter label="Clicks" />
</Island>
</main>
);
}ラッパーは既存の data-zfb-island* 境界属性を持つ <div> として残ります。通常の島には data-zfb-island、SSR をスキップする島には data-zfb-island-skip-ssr が付きます。ラッパーにはスケジュール値、data-zfb-transport="json/1"、data-zfb-protocol="zudo-react/1"、空ではない data-zfb-build 識別子、props が {} の場合も含む data-props が付きます。クライアントはコンポーネントのセットアップ前に、このメタデータを検証します。
Island は SDK の境界ヘルパーです。ランタイムのコアエントリは、ブランド付きの記述と子のモデルを公開します。
import type {
Child,
Component,
Description,
ElementType,
} from "zfb/zudo-react";Description は $$zudo ブランドとともに type、props、key を公開します。ファクトリは props をコピーし、コンポーネントは実行しません。Child は同期的なスカラー値、記述、リアクティブなスカラー値、またはそれらを入れ子にした配列です。通常のオブジェクトスプレッドによる記述のコピーはサポートされますが、ブランドのない類似オブジェクトの自作はサポートされません。
島の境界
空の JSX 子を取り除いた後、ラッパーが受け取るのは、登録済みの同期関数コンポーネントを型とする記述 1 つだけです。生の文字列、ホスト要素、複数のコンポーネント記述、未登録の関数、入れ子の島境界はサーバーレンダリング時に失敗します。子コンポーネントには空でない props.children を渡せません。children は暗黙には島の境界を越えません。null、undefined、boolean、再帰的に空の配列は空として省略されます。
スキャナーがマーカー名に使うのは、定義元の関数自身の名前(type.name)です。スキャナーは displayName を読み取らず、import や re-export の alias でもマーカーは変わりません。サーバーレンダリング時、displayName は未設定か関数名と一致している必要があります。異なる値ではターゲット名が変わらず、ZR_ISLAND_IDENTITY で失敗します。匿名または未登録の名前も、クライアントエントリを推測せずに失敗します。クライアントもセットアップ前に、ラッパーの同じコンポーネント名と空ではないビルド識別子を検証します。静的な検出ルールは島のガイドを参照してください。
ビルド識別子と登録済みの名前は、zfb のサーバーバンドルが設定するメタデータから読み取ります。別のバンドラーや素の Node で Island をレンダリングする場合は、zfb ビルドの外で Island をレンダリングするを参照してください。
data-props を越えられるのは JSON データだけです。トップレベルの値はプレーンなレコードでなければなりません。通常のレコードと null プロトタイプのレコードを受け付けます。入れ子の値は null、boolean、string、有限の number、密な配列、プレーンなレコードを使えます。繰り返し参照は値としてシリアライズされます。getter や独自の toJSON は呼び出しません。
レコードが自身に持つ列挙可能な文字列キーのデータプロパティで、値が undefined のものは再帰的に省略されます。サーバーコンポーネントと JSON transport には同じ正規化済み props が渡るため、省略された任意キーはパース後もハイドレーション中も存在しません。配列内のレコードにも同じ規則が適用されますが、配列の要素が削除されたり位置がずれたりすることはありません。
明示的な null は、自身のプロパティとして保持されます。undefined の配列要素と配列の穴は拒否されます。関数、symbol、bigint、非有限数、循環参照、クラスのインスタンス、date、map、set、DOM ノード、promise、記述、signal も拒否されます。アクセサー、列挙不能または symbol のプロパティ、自身のキー __proto__、prototype、constructor も拒否します。入れ子の children プロパティは通常の JSON データです。省略されるのはラッパーのトップレベルにある空の children だけです。不正なサーバーペイロードはページのレンダリングを失敗させ、不正なクライアントペイロードはその島だけを失敗させます。
ラッパー要素と data-zfb-island* 名は、セレクターと永続化のために維持されます。ハイドレーション用のコメント領域は島の部分ツリー内だけに発行されます。手書きの永続ラッパーも、完全なマーカー、スケジュール、transport、protocol、build、props のメタデータを持ち、内部 HTML が renderToString(child, { island: identity }) でレンダリングされていればサポートされます。一部だけを備えた旧ラッパーには、不足するメタデータを推測して補いません。そのラッパーでは、既存の data-zfb-transition-persist と data-zfb-transition-persist-props 属性も使えます。
ハイドレーション戦略
when prop は 4 つの値を受け付けます。デフォルトは "load" で、発行されるラッパーには常に data-when 値が付きます。
| 値 | 振る舞い |
|---|---|
"load" | クライアントバンドルの実行後すぐにサーバー DOM を引き受けます。 |
"visible" | ラッパーがビューポートに入ったときに引き受けます。 |
"idle" | ブラウザーの次の idle コールバックで引き受けます。 |
"media" | 必須の media クエリが最初に一致したときに引き受けます。 |
media によるスケジュールには、戦略と空ではないクエリの両方を渡します。
<Island when="media" media="(max-width: 768px)">
<MobileMenu />
</Island>ルートが除去されれば、ブラウザーは待機中のトリガーを取り消せます。必要なスケジュール API が利用できない場合、ランタイムは即時実行にフォールバックします。同じスケジュール規則が SSR スキップの島にも適用されます。
SSR スキップモード
子をサーバーで実行してはいけない場合は ssrFallback を渡します。サーバーは識別子と props を検査し、ハイドレーションマーカーなしでフォールバックをレンダリングします。クライアントのトリガーが発火すると、mount は DOM 外で子を構築し、セットアップと検証が成功した後にだけフォールバックを置き換えます。
import { Island } from "zfb";
import HeavyChart from "./HeavyChart";
export default function Page() {
return (
<Island ssrFallback={<div>Loading chart…</div>} when="visible">
<HeavyChart data={chartData} />
</Island>
);
}セットアップが失敗するか起動が取り消された場合、フォールバックはそのまま残ります。
ナビゲーションをまたいで島を永続化する
島のラッパーに安定したキーを付けるには persist を渡します。クライアントルーターのナビゲーションをまたいで島を維持したい各ページで、同じ空でなく前後に空白のないキーを使います。キーは各ドキュメント内で一意にしてください。SDK は所有するラッパーにキーを出力します。予約済みの transport や識別属性を呼び出し側の props として公開しません。
<Island persist="desktop-sidebar" persistProps>
<SidebarTree {...props} />
</Island>デフォルトでは、props が変わると古いルートを一度破棄し、受信 props で新しいルートをマウントします。props が同じなら現在のルートと状態を維持します。受信ページの props が異なっても古い props と状態を保つには persistProps を true にします。persistProps のデフォルトは false で、persist が必要です。コンポーネント、ビルド、transport、protocol、ルートの種類が変わる場合は、ルートを作り直します。
マウント後のマーカー
ランタイムはルートのマウントに成功した後だけ data-zfb-island-mounted を書き込みます。このマーカーは観測用です。重複マウントを防ぐのは、ラッパーに保存された生きたハンドルです。コンポーネントが始めた任意の非同期処理の完了を保証するものではありません。
dispose() は signal、effect、リスナーを解放しますが、子 DOM は残します。unmount() は破棄とともにルートが所有するノードを除去し、ラッパーを残します。ナビゲーション中、変更のない永続化された島は生きたルートを保持します。コンポーネント識別子の変更や props の更新があれば、古いルートを破棄し、置き換えには hydrate ではなく render を使います。
"use client" ディレクティブ
呼び出し元でコンポーネントをラップする代わりに、.tsx ファイルの最初の文に "use client" ディレクティブを書けます。ビルドスキャナーはそのコンポーネントを島のエントリとして登録します。
"use client";
import { signal } from "zfb/zudo-react";
export default function Counter() {
const count = signal(0);
return (
<button on:click={() => { count.value += 1; }}>
Count: {count}
</button>
);
}生成されたクライアントマニフェストは、検証済みの JSON props とラッパーを受け取ります。その mount 関数は、既存の子 DOM を引き受けるときは "hydrate"、新しい部分ツリーを作るときは "render" を選びます。事前検査が失敗すると null を返し、サーバー DOM は非活性のまま残します。破壊的なクライアントレンダリングにはフォールバックしません。
島の境界を使う場面については島、ランタイムのコンポーネントモデルについてはzudo-react の概要を参照してください。
落とし穴: 島を自己ラップしない
すでに島を所有するコンポーネント内に <Island> 境界を置かないでください。2 つの独立した所有者が同じ DOM 部分ツリーを安全に引き受けることはできないため、入れ子のルートは拒否されます。
// Page.tsx — place the boundary at the call site.
<Island when="idle">
<Counter />
</Island>