Skip to content

サーバーサイドレンダリング(SSR)

PreffX はサーバー上でコンポーネントを HTML 文字列にレンダリングし、その後クライアント上で同じツリーをハイドレーションします。サーバーはマークアップと、解決済みの非同期データを含むルートごとのプリロードスクリプトを出力します。クライアントはそのスクリプトを読み取り、再取得する代わりに値をツリーに供給します。

SSR ユーティリティは preffx/server からインポートします。

サーバー上の createRoot

サーバーは独自の createRoot をエクスポートします。これはクライアントのものをミラーしますが、mount ではなく renderToString メソッドを返します。

ts
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(ルーティング、言語)やリクエストスコープのコンテキストに依存する可能性があるためです:

ts
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?)

クライアントルートと同じルートオプションを受け入れるため、設定が同期されたままになります:

オプションタイプ説明
defaultURLstring | URLルーティングの初期 URL
defaultLangstringi18n の初期言語
prefixstringルートごとのプレフィックス(クライアントルートと一致させる必要があります)
contextobjectツリーと共有されるリクエストスコープのコンテキスト

単一のメソッドを持つオブジェクトを返します:

renderToString(component, config?)

  • component — ルートコンポーネント(PC または APC)。
  • config — 任意で、以下を含みます:
    • props — ルートコンポーネントへの props。
    • serializer — プリロードデータをシリアライズするカスタム関数(デフォルトは JSON.stringify)。

{ preload, serialize } を返します:

メソッドシグネチャ説明
preloadpreload(timeout?)登録されたすべての非同期リソースを解決します。任意のタイムアウトにより、遅いリソースは「ベストエフォート」になり、ページは依然としてレンダリングされ、クライアントが再取得します。
serializeserialize(): stringHTML 文字列(マークアップ + ルートごとのプリロードデータスクリプト)を出力します。

サーバー上のリソース

コンポーネントの内部で、resource はサーバー上で透過的に動作します: effect を実行する代わりに、自身のフェッチャーを preload() 用に登録します。サーバーとクライアントの両方で同じコンポーネントを再利用してください — 分岐は不要です。

tsx
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.valuenullpendingtrue であるため、フォールバック('loading')がレンダリングされます。
  • preload() の後、解決された値はマークアップに埋め込まれ、serialize() もそれをプリロードデータスクリプトに書き込みます。
  • ハイドレーション中のクライアントでは、リソースはプリロードされた値を読み取り、取得をスキップして pendingfalse に保ちます。

クライアントでのハイドレーション

クライアントの createRoot().mount() は、サーバーが出力したプリロードデータスクリプトを検出し、それをツリーに供給して、再作成する代わりに既存の DOM をハイドレーションします — 追加の呼び出しは不要です。

tsx
import { createRoot } from 'preffx';
import { App } from './App';

createRoot().mount(App, {
    node: document.getElementById('app')!
});

ハイドレーションを成功させるための要件:

  • クライアントの createRoot オプション(prefixdefaultLangdefaultURLcontext)はサーバーと一致している必要があります。
  • レンダリングされたマークアップは、クライアントが期待する正確な場所(mount が参照するコンテナ)に挿入される必要があります。

例: 完全なリクエストフロー

tsx
// 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() };
}
tsx
// entry-client.tsx
import { createRoot } from 'preffx';
import { App } from './App';

createRoot().mount(App, { node: document.getElementById('app')! });

Node サーバーは appHtml を HTML シェル(例: index.html)にレンダリングしてブラウザに送信し、その後クライアントスクリプトが同じツリーをハイドレーションします。

Apache License 2.0に基づいて公開されています。