Remix+CloudflareでWebサイトを作る 41(dangerouslySetInnerHTML削除、next-themes、原因切り分けのためにVercelにもデプロイ)
dangerouslySetInnerHTML
を使わないコードに修正する
【2024-10-31】背景
ところどころで使っているがBiomeに怒られている。
書いてみる
以下のようにしてみた。
pnpm add html-react-parser isomorphic-dompurify
import parse from "html-react-parser";
import DOMPurify from "isomorphic-dompurify";
// loaderは省略
export default function Page() {
const { html } = useLoaderData<typeof loader>();
const sanitizedHtml = DOMPurify.sanitize(html);
return (
<div>
<div{parse(sanitizedHtml)}</div>
</div>
);
}
注意点
isomorphic-dompurify
ではなく dompurify
を使うと以下のエラーが出る。
TypeError: __vite_ssr_import_3__.default.sanitize is not a function\
SSRゆえに起こる問題っぽいのでisomorphic-dompurify
を使う。
こっちを使うことは公式のREADMEにも書かれている。
If you have problems making it work in your specific setup, consider looking at the amazing isomorphic-dompurify project which solves lots of problems people might run into.
参考までにトレンド推移。
エラーが発生
デプロイ時に以下のエラーが発生。
✘ [ERROR] Deployment failed!
Failed to publish your Function. Got error: Uncaught ReferenceError: window is not defined
at functionsWorker-0.5473523066909975.js:84924:22 in ../node_modules/isomorphic-dompurify/browser.js
Remixで使えないよ、というIssueがremix-runにたっている(2024-01-30)。
Issue内にはisomorphic-dompurifyにIssueたてたよ、と書かれているので見に行ってみるが、そっちではRemix側の問題なのでCloseということになっている。
う〜〜んわからん。
一旦サニタイズはスキップするようにした。
action()
を発火させているため、再検証のため現在のページのloader()
が発火してしまう
【2024-11-01】テーマ(Light/Dark)をトグルで切り替えて永続化させるために背景
ここでテーマをLight/Darkで変えられるようにした。
しかし、action()
を発火させているコードがあるため、再検証が実行されている。
つまり、テーマを変更したページのloader()
が走ってしまっている。
解決策として shouldRevalidate
が最初に思い浮かんだけど、どのページでも起こり得るから全く根本的な解決方法ではない。
前のScrapで参考にしたページのコードでいうと以下の部分が問題。
The Complete Guide to Dark Mode with Remix | Matt Stobbs
function ThemeProvider({ children }: { children: ReactNode }) {
// ...
useEffect(() => {
if (!mountRun.current) {
mountRun.current = true;
return;
}
if (!theme) {
return;
}
// ここでPOSTしてaction()を発火させている
persistThemeRef.current.submit({ theme }, { action: 'action/set-theme', method: 'post' });
}, [theme]);
}
解決方法
これを使おう。
↑使い方。
【2024-11-09】VercelでデプロイしたらTTFBはどう変化するのか
背景
1つ前のScrapのTTFBで3.8sかかってる問題の続き。
原因の切り分けのためにVercelにデプロイして計測してみる。
Vercelへのデプロイ
設定ファイル書き換え
pnpm i @vercel/remix
export default defineConfig(({ mode }) => {
return {
plugins: [
envOnlyMacros(),
- remixCloudflareDevProxy({ getLoadContext }),
remix({
routes,
future: {
v3_singleFetch: true,
},
+ presets: [vercelPreset()],
}),
tsconfigPaths(),
],
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
- "types": ["@remix-run/cloudflare", "vite/client"],
+ "types": ["@vercel/remix", "vite/client"],
@remix-run/cloudflare
→ @vercel/remix
約100箇所、ひたすらに置換。
@remix-run/cloudflare
は1ヶ月前に更新されていてv2.13.1なのに、@remix-run/vecel
は最終更新が2023-08と、1年以上前でv1.19.3で止まってる。
「Remix v2.13使ってるからSingle Fetch使ってる書き方してるけど@remix-run/vercel
使って動くのかな」か思ったけどそれをやるのは @vercel/remix
の方。
なかなかうまくいかない
全部Vercel用に書き換えるの骨折れる。
別のRepositoryを作って完全に新しく作成して少しずつコピペしていくほうが早い気がしてきた。
【2024-11-10】Vercelだとdeferで取得するようなPromise型のデータが画面上に表示されない問題
背景
CloudflareからVercel用に色々書き換えたらdeferで取得しているデータに関してずっと<Suspence>
のfallback
の状態になってしまいデータが表示されない。
解決方法
entry.server.tsx
でrenderToString
を使わずにrenderToPipeableStream
を使う。
Before
import { RemixServer } from "@remix-run/react";
import { createInstance } from "i18next";
import { initReactI18next } from "react-i18next";
import { renderToString } from "react-dom/server";
import { I18nextProvider } from "react-i18next";
import { i18nextConfig } from "~/config/i18n";
import i18nServer from "~/services/i18n.server";
import type { AppLoadContext, EntryContext } from "@vercel/remix";
export default async function handleRequest(
request: Request,
initialResponseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
loadContext: AppLoadContext,
) {
const body = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: initialResponseStatusCode,
});
}
After
import { PassThrough } from "node:stream";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import type { AppLoadContext, EntryContext } from "@vercel/remix";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext,
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}
【2024-11-11】Vercel用にGitHub Actionsを修正
ここを参考に以下3つをGitHub Secrets Variablesとして登録してStagingにデプロイするGitHub Actionsを修正。
- VERCEL_ORG_ID
- VERCEL_PROJECT_ID
- VERCEL_TOKEN
Vercel CLI 37.14.0
No Project Settings found locally. Run `vercel pull --yes` to retrieve them.
Error: Process completed with exit code 1.
このエラーが消えない。
丸1日近くの時間スタックした。
色々直してやっとVercelにデプロイした。
ちょっと遅い気がするけどさすがに3800msもかからない。
デプロイ1
初回
2回目
デプロイ2
初回
デプロイ3
初回
Warning: Extra attributes from the server: class,style
【2024-11-13】エラー内容
[Error] Warning: Extra attributes from the server: class,style
html
App@http://localhost:5173/app/root.tsx:205:11
AppWithProviders@http://localhost:5173/app/root.tsx:333:29
RenderedRoute@http://localhost:5173/node_modules/.vite/deps/chunk-AEHPLVJH.js:402:11
RenderErrorBoundary@http://localhost:5173/node_modules/.vite/deps/chunk-AEHPLVJH.js:359:10
...
調査
修正方法がない?
next-theme
を入れると起こるWarning。
これはDarkModeなどのテーマを切り替えるような場合、使用しているUIライブラリに関係なく起こります。
根本的な解決策はないが、エラーの抑制をする事で対応する。
まじか。
<body suppressHydrationWarning={true}>
とすることでコンソールにログを出さないようにすることができるようだが...消えないな??
解決方法
この方法で対応した
ん〜わからん。違うリポジトリで1から作り直していって原因特定したほうが早い気がしてきた。