Island
Island JSX ラッパーを使って、コンポーネントをクライアントでハイドレートされる島としてラップします。
<Island> ラッパー
"@takazudo/zfb" から Island をインポートし、ブラウザでハイドレートしたいコンポーネントをラップします。それ以外はサーバーレンダリングされた HTML のままになります。
import { Island } from "@takazudo/zfb";
import Counter from "./Counter";
export default function Page() {
return (
<main>
<h1>My Page</h1>
<Island when="visible">
<Counter />
</Island>
</main>
);
}SSR 時、<Island> ラッパーは <div data-zfb-island="Counter" data-when="visible"> マーカーを発行し、その内側にサーバーレンダリングされた子要素を配置します。クライアントランタイム(@takazudo/zfb-runtime)はこれらのマーカーを問い合わせ、when 条件が発火したときにコンポーネントをマウントします。
IslandProps
interface IslandProps {
when?: When;
media?: string;
ssrFallback?: VNode;
children?: VNode;
}when— ハイドレーション戦略。デフォルトは"load"。後述の ハイドレーション戦略 を参照してください。media—when="media"島用の CSS メディアクエリ文字列。このクエリが最初にマッチしたとき(リサイズや回転による後続のビューポート変更を含む)にハイドレートします。when="media"のときに必須で、他の戦略には無視されます(開発時に警告が出ます)。ssrFallback— SSR スキップモードを有効にします(Astro のclient:onlyに相当)。指定すると、重いchildrenはサーバーサイドで評価されません。代わりにssrFallbackがレンダリングされ、クライアントはハイドレーション時に本物のコンポーネントに差し替えます。children— ハイドレートするコンポーネント。
ハイドレーション戦略
when プロップは 4 つの値を受け付けます。いずれも現在出荷済みです。
| 値 | 振る舞い |
|---|---|
"load" | ページの JavaScript が実行されたら即座にハイドレートします。when を省略した場合のデフォルトです。 |
"visible" | 島のルート要素がビューポートに入ったときにハイドレートします(IntersectionObserver、しきい値 0)。画面外コンテンツに対して最も低コストの遅延手段です。 |
"idle" | ブラウザの次のアイドルコールバック時にハイドレートします。requestIdleCallback がないプラットフォームでは setTimeout(0) にフォールバックします。 |
"media" | CSS メディアクエリが最初にマッチしたときにハイドレートします(window.matchMedia)。同伴の media プロップが必要です。後続のビューポート変更(リサイズ、画面向き変更)にも対応します。matchMedia が使用できない場合は即座にハイドレートにフォールバックします。注意: SSR スキップ(ssrFallback)島は when を完全に無視し、即座にレンダリングします。 |
// 狭いビューポート(例: モバイルメニュー)でのみハイドレートする
<Island when="media" media="(max-width: 768px)">
<MobileMenu />
</Island>SSR スキップモード
ssrFallback を渡すと、重い子要素のサーバーレンダリングを完全にスキップできます。
import { Island } from "@takazudo/zfb";
import HeavyChart from "./HeavyChart";
export default function Page() {
return (
<Island ssrFallback={<div>Loading chart…</div>}>
<HeavyChart data={data} />
</Island>
);
}サーバーは <div data-zfb-island-skip-ssr="HeavyChart" data-when="load">…fallback…</div> を発行します。ハイドレーション時にクライアントランタイムがプレースホルダーへ HeavyChart をレンダリングします。
VNode 型のエクスポート
VNode、VNodeArray、VNodeObject は "@takazudo/zfb" からパブリック型としてエクスポートされます。
import type { VNode, VNodeArray, VNodeObject } from "@takazudo/zfb";これらの構造的型はフレームワーク非依存です。VNode には素の object メンバーが含まれており(Preact 自身の ComponentChild 設計と同じパターン)、Preact の ComponentChildren・VNode<Props>・JSX.Element・JSX.Element[] は Island の入力境界(children と ssrFallback)に as unknown as キャストなしで直接代入できます。
import type { ComponentChildren } from "preact";
import { Island } from "@takazudo/zfb";
// ComponentChildren を children として転送 — キャスト不要
function Wrapper({ children }: { children: ComponentChildren }) {
return <Island when="load">{children}</Island>;
}
// ComponentChildren を ssrFallback として転送 — キャスト不要
function SkipWrapper({ fallback }: { fallback: ComponentChildren }) {
return (
<Island ssrFallback={fallback}>
<HeavyWidget />
</Island>
);
}スロットを ComponentChildren として型注釈する(返却方向)
スロット変数が <Island> の返り値を保持する場合は、型を IslandElement ではなく ComponentChildren や JSX.Element で注釈してください。IslandElement は props.children を必須とする意図的な狭い構造型であり、Preact の VNode<{}> はこれに代入できません。代わりに Preact ネイティブの型を使用してください。
import type { ComponentChildren } from "preact";
function Page() {
const slot: ComponentChildren = (
<Island when="visible">
<Counter />
</Island>
);
return <main>{slot}</main>;
}Preact 利用者向けの名前衝突に関する注意
ファイルがすでに import { VNode } from "preact" を持っている場合は、名前の衝突を避けるために修飾インポートを使用してください。
import type { VNode as ZfbVNode } from "@takazudo/zfb";エクスポートされる定数とヘルパー
以下は Island とともに "@takazudo/zfb" からエクスポートされます。
HYDRATE_MARKER_ATTR—data-zfb-island属性名。SKIP_SSR_MARKER_ATTR—data-zfb-island-skip-ssr属性名。ANONYMOUS_COMPONENT_NAME— ラップした子要素の素性を特定できないときに使われるフォールバック名。resolveWhen(when: unknown): When—when値を検証して正規化します。不明な入力は"load"にフォールバックします(開発時は警告つき)。
"use client" ディレクティブ
zfb は代替の記述スタイルとして、ファイルレベルの "use client" ディレクティブもサポートします。最初の文がリテラル文字列 "use client" であるコンポーネントファイルは、ビルドスキャナ(crates/zfb-islands)によって島のエントリとして扱われます。esbuild ベースのバンドラは、それを独自の依存グラフとともに ESM へ個別にコンパイルします。
"use client";
import { useState } from "preact/hooks";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}サーバーコンポーネントやページモジュールから島をインポートします。ビルドパイプラインがハイドレーションのブートストラップを自動的に配線します。
島のアーキテクチャ全般や、いつ島に手を伸ばすべきかについては Islands を参照してください。
落とし穴: 島を自己ラップしない
ネストされた島マーカーはハイドレーションの不具合を引き起こします
コンポーネント自身のレンダリング出力に <Island> ラッパーを置かないでください。ランタイムは DOM 内のすべての島マーカーを検出して各コンポーネントをマウントします。マーカーが別のマーカーの内側にネストされていると、2 つの独立したフレームワークインスタンスが同じ DOM サブツリーを所有しようとして競合が発生します。
誤り — コンポーネント自身の中に <Island> を置く:
/ / Counter. tsx — こうしてはいけません export default function Counter() { return ( <Island when= "idle"> <InnerWidget / > </ Island> ); }正しい — 呼び出し元で <Island> を適用する:
/ / Page. tsx <Island when= "idle"> <Counter / > </ Island>開発時、このパターンを検出するとランタイムが問題のあるコンポーネント名を示す console.warn を出力します。