空雲 Blog

Eye catchRemix + Cloudflare で環境変数などをクライアントへ引き渡す

publication: 2024/08/04
update:2024/08/06

Cloudflare と環境変数

Cloudflare の Pages や Workers は、プロセス起動時点では環境変数が設定されていません。各エンドポイントが呼び出された時点で、環境変数が設定されます。そのため、サーバーサイドで環境変数を使う場合は、リクエスト毎に環境変数を取得する必要があります。

クライアント側に環境変数の値を真っ当に引き渡そうとすると、routes ごとに loader を設置して、ページごとに環境変数を配布しなければなりません。これは非常に面倒です。Remix の公式ドキュメントでは以下のようにやり方が紹介されています。

https://remix.run/docs/en/main/guides/envvars#browser-environment-variables

ここに含まれている以下のコードは実は致命的な問題があって、データに</script>という文字列が含まれていた場合、タグが終了してデータがぶった切れます。このようなデータの引き渡しを正常に処理するなら「<」 をパースする必要があります。

1<script
2 dangerouslySetInnerHTML="{{"
3 __html:
4 `window.ENV="${JSON.stringify("
5 data.ENV
6 )}`,
7 }}
8/>

今回は、必要な設定を最初に埋め込むだけで、クライアントに環境変数配る方法を紹介します。もちろん上記のような問題も対処済みです。

使用パッケージ

必要な機能をまとめたパッケージを作りました

https://www.npmjs.com/package/remix-provider

実装例

  • source code

https://github.com/SoraKumo001/remix-provider

  • execution result

https://remix-provider.pages.dev/

entry.server.tsx

ServerProvider を設置して、クライアントに渡したいデータを設定します。

初回リクエスト時にデータを設定するため、routes ごとに loader を設置する必要はありません。

1import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
2import { RemixServer } from "@remix-run/react";
3import { isbot } from "isbot";
4import { renderToReadableStream } from "react-dom/server";
5import { ServerProvider } from "remix-provider";
6
7export default async function handleRequest(
8 request: Request,
9 responseStatusCode: number,
10 responseHeaders: Headers,
11 remixContext: EntryContext,
12 loadContext: AppLoadContext
13) {
14 const body = await renderToReadableStream(
15 // Set the values you want to distribute to clients.
16 <ServerProvider
17 value={{
18 env: loadContext.cloudflare.env,
19 host: request.headers.get("host"),
20 }}
21 >
22 <RemixServer context={remixContext} url={request.url} />
23 </ServerProvider>,
24 {
25 signal: request.signal,
26 onError(error: unknown) {
27 // Log streaming rendering errors from inside the shell
28 console.error(error);
29 responseStatusCode = 500;
30 },
31 }
32 );
33
34 if (isbot(request.headers.get("user-agent") || "")) {
35 await body.allReady;
36 }
37
38 responseHeaders.set("Content-Type", "text/html");
39 return new Response(body, {
40 headers: responseHeaders,
41 status: responseStatusCode,
42 });
43}

root.tsx

RootProvider と RootValue の設置を行います。RootValue には、サーバから送られたデータが格納されます。

1import {
2 Links,
3 Meta,
4 Outlet,
5 Scripts,
6 ScrollRestoration,
7} from "@remix-run/react";
8import "./tailwind.css";
9import { RootProvider, RootValue } from "remix-provider";
10
11export function Layout({ children }: { children: React.ReactNode }) {
12 return (
13 // Additional providers.
14 <RootProvider>
15 <html lang="en">
16 <head>
17 <meta charSet="utf-8" />
18 <meta name="viewport" content="width=device-width, initial-scale=1" />
19 <Meta />
20 <Links />
21 {/* Install components to transfer data to clients. */}
22 <RootValue />
23 </head>
24 <body>
25 {children}
26 <ScrollRestoration />
27 <Scripts />
28 </body>
29 </html>
30 </RootProvider>
31 );
32}
33
34export default function App() {
35 return <Outlet />;
36}

routes/_index.tsx

各コンポーネントでデータを取得する場合は useRootContext を使います。ServerProvider で設定した値が取得できます。

データは環境変数に限らず、任意の値を設定し受け取ることができます。エンドポイントリクエスト時のヘッダ類や cookie の値などもデータとして渡すことができます。

1import { useRootContext } from "remix-provider";
2
3export default function Index() {
4 // Get the value distributed to clients.
5 const value = useRootContext();
6 return <div className="whitespace-pre">{JSON.stringify(value, null, 2)}</div>;
7}

Execution Result

  • Output

1{
2 "env": {
3 "ASSETS": {},
4 "CF_PAGES": "1",
5 "CF_PAGES_BRANCH": "master",
6 "CF_PAGES_COMMIT_SHA": "dfc64ad01b02b6832fae2fd3a61453ac14f6fb35",
7 "CF_PAGES_URL": "https://f3f206fa.remix-provider.pages.dev"
8 },
9 "host": "remix-provider.pages.dev"
10}

まとめ

簡単にサーバー側のデータをクライアントに配ることが出来ました。これにより、環境変数やリクエストヘッダなどをクライアント側で利用することが可能になります。

Remix + Cloudflare でブログシステムを作成する過程で作ったものを、今回、機能を分離させてパッケージ化しました。同じようパッケージが他に見当たらないのが不思議なのですが、他の方々は loader を書いて環境変数を配っているのでしょうか?

https://next-blog.croud.jp/contents/eec22faf-3563-4a77-9a47-4dfddc604141

こちらで記事を書いていますが、Remix を便利に使う機能から Prisma の容量問題の解決や画像最適化まで、必要な機能は全部その時に作りました。足りない機能はサクッと作るのが一番早いです。