Remix + Cloudflare でloaderを使わず、環境変数などをクライアントへ引き渡す方法
Cloudflare と環境変数
Cloudflare の Pages や Workers は、プロセス起動時点では環境変数が設定されていません。各エンドポイントが呼び出された時点で、環境変数が設定されます。そのため、サーバーサイドで環境変数を使う場合は、リクエスト毎に環境変数を取得する必要があります。
クライアント側に環境変数の値を真っ当に引き渡そうとすると、routes ごとに loader を設置して、ページごとに環境変数を配布しなければなりません。これは非常に面倒です。Remix の公式ドキュメントでは以下のようにやり方が紹介されています。
ここに含まれている以下のコードは実は致命的な問題があって、データに</script>
という文字列が含まれていた場合、タグが終了してデータがぶった切れます。このようなデータの引き渡しを正常に処理するなら「<」 をパースする必要があります。
<script
dangerouslySetInnerHTML="{{"
__html:
`window.ENV="${JSON.stringify("
data.ENV
)}`,
}}
/>
今回は、必要な設定を最初に埋め込むだけで、クライアントに環境変数配る方法を紹介します。もちろん上記のような問題も対処済みです。
使用パッケージ
必要な機能をまとめたパッケージを作りました
実装例
- source code
- execution result
entry.server.tsx
ServerProvider を設置して、クライアントに渡したいデータを設定します。初回リクエスト時にデータを設定するため、routes ごとに loader を設置する必要はありません。また、環境変数の他にサンプルとしてリクエストヘッダから host も渡しています。もちろん cookie なども引き渡し可能です。
import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";
import { ServerProvider } from "remix-provider";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
const body = await renderToReadableStream(
// Set the values you want to distribute to clients.
<ServerProvider
value={{
env: loadContext.cloudflare.env,
host: request.headers.get("host"),
}}
>
<RemixServer context={remixContext} url={request.url} />
</ServerProvider>,
{
signal: request.signal,
onError(error: unknown) {
// Log streaming rendering errors from inside the shell
console.error(error);
responseStatusCode = 500;
},
}
);
if (isbot(request.headers.get("user-agent") || "")) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
root.tsx
RootProvider と RootValue の設置を行います。RootValue には、サーバから送られたデータが格納されます。
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import "./tailwind.css";
import { RootProvider, RootValue } from "remix-provider";
export function Layout({ children }: { children: React.ReactNode }) {
return (
// Additional providers.
<RootProvider>
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
{/* Install components to transfer data to clients. */}
<RootValue />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
</RootProvider>
);
}
export default function App() {
return <Outlet />;
}
routes/_index.tsx
各コンポーネントでデータを取得する場合は useRootContext を使います。ServerProvider で設定した値が取得できます。
データは環境変数に限らず、任意の値を設定し受け取ることができます。エンドポイントリクエスト時のヘッダ類や cookie の値などもデータとして渡すことができます。
import { useRootContext } from "remix-provider";
export default function Index() {
// Get the value distributed to clients.
const value = useRootContext();
return <div className="whitespace-pre">{JSON.stringify(value, null, 2)}</div>;
}
Execution Result
- Output
{
"env": {
"ASSETS": {},
"CF_PAGES": "1",
"CF_PAGES_BRANCH": "master",
"CF_PAGES_COMMIT_SHA": "dfc64ad01b02b6832fae2fd3a61453ac14f6fb35",
"CF_PAGES_URL": "https://f3f206fa.remix-provider.pages.dev"
},
"host": "remix-provider.pages.dev"
}
まとめ
簡単にサーバー側のデータをクライアントに配ることが出来ました。これにより、環境変数やリクエストヘッダなどをクライアント側で利用することが可能になります。
Remix + Cloudflare でブログシステムを作成する過程で作ったものを、今回、機能を分離させてパッケージ化しました。同じようパッケージが他に見当たらないのが不思議なのですが、他の方々は loader を書いて環境変数を配っているのでしょうか?
こちらで記事を書いていますが、Remix を便利に使う機能から Prisma の容量問題の解決や画像最適化まで、必要な機能は全部その時に作りました。足りない機能はサクッと作るのが一番早いです。
Discussion