Island
Islandは、静的ページの一部だけをReactで動かす仕組みです。ページの本文やレイアウトは静的HTMLにし、状態やイベントを持つUIに必要なJavaScriptを追加できます。
Table of Contents
プラグインを追加する
静的HTML出力に必要なpluginSsg()に加え、pluginIsland()を追加します。
import { defineConfig, pluginSsg, pluginIsland } from "minista"
export default defineConfig({
plugins: [pluginSsg(), pluginIsland()],
})TypeScriptではtsconfig.jsonのtypesにminista/clientを含めてください。テンプレートには設定済みです。
操作できるコンポーネントを作る
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を付けます。
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.tsxdist/
├── index.html
└── assets/
├── island-1-[hash].js
└── ...(ReactやCounterのJavaScriptなど)JavaScriptの分割結果は設定や依存関係で変わります。npm run buildとnpm run previewを実行し、カウントが増えることを確認してください。HTMLを直接開くより、ローカルサーバー経由で動作を確認できます。
変数をpropsとして渡す
Islandのpropsには、HTMLへ埋め込み、ブラウザ側で復元できる(シリアライズできる)値を渡せます。ページ内の変数やgetStaticData()で取得した値も、シリアライズできれば利用できます。ブラウザではpropsを復元してからハイドレーションします。同じコンポーネントを複数配置しても、それぞれの値が保持されます。
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へ置き換える方法を紹介します。