サーバーサイドレンダリング(SSR)
PreffX はサーバー上でコンポーネントを HTML 文字列にレンダリングし、その後クライアント上で同じツリーをハイドレーションします。サーバーはマークアップと、解決済みの非同期データを含むルートごとのプリロードスクリプトを出力します。クライアントはそのスクリプトを読み取り、再取得する代わりに値をツリーに供給します。
SSR ユーティリティは preffx/server からインポートします。
サーバー上の createRoot
サーバーは独自の createRoot をエクスポートします。これはクライアントのものをミラーしますが、mount ではなく renderToString メソッドを返します。
import { createRoot } from 'preffx/server';
import { App } from './App';
const server = createRoot();
const { preload, serialize } = server.renderToString(App, {
props: { name: 'PreffX' }
});
await preload(); // resolve every async resource
const html = serialize(); // markup + preload data script通常はリクエストごとに新しいルートが作成されます。アプリケーションがリクエスト URL(ルーティング、言語)やリクエストスコープのコンテキストに依存する可能性があるためです:
import { createRoot } from 'preffx/server';
export async function render(url: URL) {
const root = createRoot({
defaultURL: url,
defaultLang: 'en',
context: { tenant: 'acme' }
});
const { preload, serialize } = root.renderToString(App, {});
await preload();
return { appHtml: serialize() };
}API
createRoot(config?)
クライアントルートと同じルートオプションを受け入れるため、設定が同期されたままになります:
| オプション | タイプ | 説明 |
|---|---|---|
defaultURL | string | URL | ルーティングの初期 URL |
defaultLang | string | i18n の初期言語 |
prefix | string | ルートごとのプレフィックス(クライアントルートと一致させる必要があります) |
context | object | ツリーと共有されるリクエストスコープのコンテキスト |
単一のメソッドを持つオブジェクトを返します:
renderToString(component, config?)
component— ルートコンポーネント(PCまたはAPC)。config— 任意で、以下を含みます:props— ルートコンポーネントへの props。serializer— プリロードデータをシリアライズするカスタム関数(デフォルトはJSON.stringify)。
{ preload, serialize } を返します:
| メソッド | シグネチャ | 説明 |
|---|---|---|
preload | preload(timeout?) | 登録されたすべての非同期リソースを解決します。任意のタイムアウトにより、遅いリソースは「ベストエフォート」になり、ページは依然としてレンダリングされ、クライアントが再取得します。 |
serialize | serialize(): string | HTML 文字列(マークアップ + ルートごとのプリロードデータスクリプト)を出力します。 |
サーバー上のリソース
コンポーネントの内部で、resource はサーバー上で透過的に動作します: effect を実行する代わりに、自身のフェッチャーを preload() 用に登録します。サーバーとクライアントの両方で同じコンポーネントを再利用してください — 分岐は不要です。
import type { PC } from 'preffx';
export const UserCard: PC = (_props, { resource, computed }) => {
const [user] = resource(async () => {
const res = await fetch('/api/user');
return res.json();
});
const display = computed(() => user.state.value?.name ?? 'loading');
return (
<article className="user">
<h1>{display}</h1>
</article>
);
};preload()が実行される前は、user.state.valueはnull、pendingはtrueであるため、フォールバック('loading')がレンダリングされます。preload()の後、解決された値はマークアップに埋め込まれ、serialize()もそれをプリロードデータスクリプトに書き込みます。- ハイドレーション中のクライアントでは、リソースはプリロードされた値を読み取り、取得をスキップして
pendingをfalseに保ちます。
クライアントでのハイドレーション
クライアントの createRoot().mount() は、サーバーが出力したプリロードデータスクリプトを検出し、それをツリーに供給して、再作成する代わりに既存の DOM をハイドレーションします — 追加の呼び出しは不要です。
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, {
node: document.getElementById('app')!
});ハイドレーションを成功させるための要件:
- クライアントの
createRootオプション(prefix、defaultLang、defaultURL、context)はサーバーと一致している必要があります。 - レンダリングされたマークアップは、クライアントが期待する正確な場所(
mountが参照するコンテナ)に挿入される必要があります。
例: 完全なリクエストフロー
// entry-server.tsx
import { createRoot } from 'preffx/server';
import { App } from './App';
export async function render(url: URL) {
const server = createRoot({ defaultURL: url });
const { preload, serialize } = server.renderToString(App, {});
await preload();
return { appHtml: serialize() };
}// entry-client.tsx
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, { node: document.getElementById('app')! });Node サーバーは appHtml を HTML シェル(例: index.html)にレンダリングしてブラウザに送信し、その後クライアントスクリプトが同じツリーをハイドレーションします。