Island

Islandは、静的ページの一部だけをReactで動かす仕組みです。ページの本文やレイアウトは静的HTMLにし、状態やイベントを持つUIに必要なJavaScriptを追加できます。

Table of Contents

プラグインを追加する

静的HTML出力に必要なpluginSsg()に加え、pluginIsland()を追加します。

vite.config.ts
import { defineConfig, pluginSsg, pluginIsland } from "minista"

export default defineConfig({
  plugins: [pluginSsg(), pluginIsland()],
})

TypeScriptではtsconfig.jsonのtypesにminista/clientを含めてください。テンプレートには設定済みです。

操作できるコンポーネントを作る

src/components/counter.tsx
import { useState } from "react"

type Props = { initialCount?: number }

export default function Counter({ initialCount = 0 }: Props) {
  const [count, setCount] = useState(initialCount)
  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      カウント:{count}
    </button>
  )
}

ページからimportし、client:loadを付けます。

src/pages/index.tsx
import Counter from "../components/counter"

export default function Page() {
  return (
    <>
      <h1>カウンター</h1>
      <p>この見出しと説明文は静的HTMLです。</p>
      <Counter initialCount={3} client:load />
    </>
  )
}

初期表示のHTMLに対して、ブラウザでReactを接続して操作可能にすることをハイドレーションと呼びます。client:*を付けずに静的コンポーネントとして使うと、onClickなどのイベント処理はブラウザで動作しません。

構成と出力を確認する

プロジェクト構成
src/
├── components/
│   └── counter.tsx
└── pages/
    └── index.tsx
ビルド結果の例
dist/
├── index.html
└── assets/
    ├── island-1-[hash].js
    └── ...(ReactやCounterのJavaScriptなど)

JavaScriptの分割結果は設定や依存関係で変わります。npm run buildとnpm run previewを実行し、カウントが増えることを確認してください。HTMLを直接開くより、ローカルサーバー経由で動作を確認できます。

変数をpropsとして渡す

Islandのpropsには、HTMLへ埋め込み、ブラウザ側で復元できる(シリアライズできる)値を渡せます。ページ内の変数やgetStaticData()で取得した値も、シリアライズできれば利用できます。ブラウザではpropsを復元してからハイドレーションします。同じコンポーネントを複数配置しても、それぞれの値が保持されます。

src/pages/index.tsx
import Counter from "../components/counter"

export function getStaticData() {
  return { props: { counts: [3, 7] } }
}

export default function Page({ counts }: { counts: number[] }) {
  return (
    <>
      {counts.map((count) => (
        <Counter key={count} initialCount={count} client:load />
      ))}
    </>
  )
}

ministaでシリアライズできる値は、文字列、有限の数値、真偽値、null、undefined、配列、プレーンなオブジェクトと、それらの組み合わせです。propsはブラウザへ公開されるため、秘密の値を含めないでください。

関数、Date、Map、Set、RegExp、BigInt、クラスのインスタンス、循環参照などは渡せません。未対応の値はMINISTA_ISLAND_PROPS_UNSUPPORTEDで通知されます。イベントハンドラーはページからpropsとして渡さず、Islandコンポーネント内へ置きます。

Islandにするコンポーネントは別ファイルから静的にimportしてください。HTMLタグで囲む使い方やJSX childrenにも対応します。children内のコンポーネントも、そのIslandのJSXに静的importした名前で記述します。ページ内で定義した関数コンポーネントや、変数から動的に選択するコンポーネントは対象にできません。

動かし始めるタイミングを選ぶ

ディレクティブ動作
client:loadページ読み込み時にハイドレーション
client:idleブラウザの待機時間にハイドレーション
client:visibleコンポーネントが画面内に入ったらハイドレーション
client:media="(max-width: 640px)"メディアクエリを満たしたらハイドレーション
client:only静的HTMLを生成せず、ブラウザでレンダリング

すぐに操作するUIにはclient:loadを使います。画面外のUIには、例えば次の指定ができます。

<Counter client:visible={{ rootMargin: "200px" }} />

client:visible client:media client:idleは条件が成立してから対象のJavaScriptを取得します。条件が成立する前は静的HTMLとして表示され、Reactのイベントは動作しません。

ブラウザのAPIを使う

通常のIslandはビルド時にもレンダリングされます。windowやdocumentを使う処理は、イベントハンドラーやuseEffect()の中へ置きます。初回の表示内容も、ビルド時とブラウザ側で一致させます。

client:onlyでもpropsとchildrenの式はサーバーで評価され、同じ値のシリアライズの制約が適用されます。本体の描画を省略する指定であり、importしたモジュールのトップレベルでwindowなどを使うことは保証しません。

静的HTMLを生成しないUIにはclient:onlyを使えますが、JavaScriptが動くまで本体は表示されません。読み込み中の表示が必要なら、フォールバックを用意します。

<div client:only>
  <p slot="fallback">読み込み中です…</p>
  <Counter />
</div>

Islandの範囲を決める

状態を共有するUIは一つの親コンポーネントにまとめ、その親をIslandにします。子コンポーネントへそれぞれclient:*を付ける必要はありません。同じJSX内でIslandを入れ子にするとエラーになります。

Reactを使わない小さなDOM操作は、通常のscriptで実装することもできます。オプションの詳細はpluginIslandを参照してください。

次はIslandの容量削減で、ブラウザ向けのReactをRedactやPreactへ置き換える方法を紹介します。