zfb
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

Island

作成 2026年6月24日Takeshi Takatsudo

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"。後述の ハイドレーション戦略 を参照してください。

  • mediawhen="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 型のエクスポート

VNodeVNodeArrayVNodeObject"@takazudo/zfb" からパブリック型としてエクスポートされます。

import type { VNode, VNodeArray, VNodeObject } from "@takazudo/zfb";

これらの構造的型はフレームワーク非依存です。VNode には素の object メンバーが含まれており(Preact 自身の ComponentChild 設計と同じパターン)、Preact の ComponentChildrenVNode<Props>JSX.ElementJSX.Element[]Island の入力境界(childrenssrFallback)に 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 ではなく ComponentChildrenJSX.Element で注釈してください。IslandElementprops.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_ATTRdata-zfb-island 属性名。

  • SKIP_SSR_MARKER_ATTRdata-zfb-island-skip-ssr 属性名。

  • ANONYMOUS_COMPONENT_NAME — ラップした子要素の素性を特定できないときに使われるフォールバック名。

  • resolveWhen(when: unknown): Whenwhen 値を検証して正規化します。不明な入力は "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 を出力します。

Revision History

Takeshi Takatsudo作成: 2026-06-25T05:17:25+09:00更新: 2026-06-25T05:17:25+09:00

AI Assistant

Ask a question about the documentation.