Remix+CloudflareでWebサイトを作る 44(Requestの型エラー)
load-context.ts
のRequest
の型をinterface Request
ではなくinterface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>
にしたい
【2024-11-23】背景
Cloudflare PagesからCloudflare Workersに移行中。
load-context.ts
をこのように書いた。
import type { PlatformProxy } from "wrangler";
type GetLoadContextArgs = {
request: Request;
context: {
cloudflare: Omit<PlatformProxy<Env>, "dispose" | "caches" | "cf"> & {
caches: PlatformProxy<Env>["caches"] | CacheStorage;
cf: Request["cf"]; // ここでエラー発生
};
};
};
declare module "@remix-run/cloudflare" {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface AppLoadContext extends ReturnType<typeof getLoadContext> {
// This will merge the result of `getLoadContext` into the `AppLoadContext`
}
}
export function getLoadContext({ context }: GetLoadContextArgs) {
return context;
}
Remix + Cloudflare WorkersのアプリをCLIから作ってReponseにホバーすると型がinterface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>
となっていたが、エラー発生時はinterface Request
になっていた。
解決方法
Remix + Cloudflare WorkersのアプリをCLIから作ったリポジトリと比較して、以下の3行を修正してpnpm i
をしてVSCodeをリロードしたら治った。
この3つのうちどこの修正が効いたか確かめるために元に戻していったが、元に戻してもエラーが再発しなかったためわからなかった。
最初に tsconfig.json
だけを修正しても治らなかったからpackage.json
も修正も効いたのか??
わからんが治ってしまった。
"devDependencies": {
- "@cloudflare/workers-types": "^4.20240925.0",
+ "@cloudflare/workers-types": "^4.20241112.0",
- "wrangler": "3.87.0"
+ "wrangler": "^3.78.12"
},
"compilerOptions": {
"types": [
"@remix-run/cloudflare",
"vite/client",
+ "@cloudflare/workers-types/2023-07-01"
],
【2024-11-25】最近見た良かった記事をメモ
Cloudflare
SPA, SSR, SSG
React
Drizzle
キャッシュ
<Suspence>
で表示しようとするとエラーになる
【2024-11-28】loaderの値をエラー
<Suspense>
<Await>
ではエラーがでているがTerminalを見るとDBの値は正しく取得できている。
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Uncaught Error: There was an error while hydrating this Suspense boundary. Switched to client rendering.
また、loaderの中でawaitを使って<Suspence>
を使わないようにしてみると次は以下のエラーが発生する。
Uncaught SyntaxError (at chunk-GTU7F3UE.js?v=5c439831:4544:11)
at Object.decodeInitial
Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.
The above error occurred in the <RemixBrowser> component:
at RemixBrowser
at I18nextProvider (
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
原因を探る
Prisma→Drizzle用に作ったこのリポジトリを使ってデバッグしていく。
以下のファイルを全部同じにしてみたけど、デバッグ用リポジトリだと正しく動く。
db/index.ts
db/schema.ts
routes/root.tsx
routes/entry.server.ts
routes/entry.client.ts
routes/_index/route.tsx
vite.config.ts
load-context.ts
server.ts
ほぼ同じだけどな??
全然わからん...。
もはやデバッグ用リポジトリをベースに、元のコードを追加していくほうが早い気がしてきたぞ。
解決方法
1つずつ差分確認していったけど以下が原因だった。
発見むっず〜。
- "remix-i18next": "^6.4.1",
+ "remix-i18next": "^7.0.0",
その他
next-themes
を使っている箇所でmouted &&
にしているとサーバーでもConsoleでもエラーがでないが画面が真っ白のまま何も表示されないので外した。
import { ThemeProvider as NextThemeProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
import { useEffect, useState } from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [mounted, setMounted] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
- return mounted && <NextThemeProvider {...props}>{children}</NextThemeProvider>;
+ return <NextThemeProvider {...props}>{children}</NextThemeProvider>;
}
TypeError: Cannot convert undefined or null to object
エラー: remix-i18next
のバージョン変更に伴いentry.client.ts
とentry.server.ts
でエラーがでている。
entry.client.ts
このエラーが発生するとホットリロードが実行されない。
entry.client.tsx:52 TypeError: Cannot convert undefined or null to object
at Function.values (<anonymous>)
at getInitialNamespaces (
Before
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/
import { StrictMode, startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
import { RemixBrowser } from "@remix-run/react";
import i18next from "i18next";
import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next/client";
import { i18nextConfig } from "~/config/i18n";
async function main() {
await i18next
.use(initReactI18next)
.use(resourcesToBackend(i18nextConfig.resources))
.use(I18nextBrowserLanguageDetector)
.init({
...i18nextConfig,
ns: getInitialNamespaces(),
detection: {
// htmlTagの検出のみを有効にして、remix-i18nextでサーバーサイドの言語のみを検出する
// `<html lang>` 属性を使用することで、サーバーサイドで検出された言語をクライアントに伝えることができる
order: ["htmlTag"],
// htmlTagしか使わないので、ブラウザに言語をキャッシュする理由はない
caches: [],
},
});
startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<RemixBrowser />
</StrictMode>
</I18nextProvider>,
);
});
}
main().catch((error) => console.error(error));
After
ここに書いてあるコードに変える
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import i18n from "./i18n";
import i18next from "i18next";
import { I18nextProvider, initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { getInitialNamespaces } from "remix-i18next/client";
async function hydrate() {
await i18next
.use(initReactI18next) // Tell i18next to use the react-i18next plugin
.use(LanguageDetector) // Setup a client-side language detector
.use(Backend) // Setup your backend
.init({
...i18n, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
ns: getInitialNamespaces(),
backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" },
detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
// we can communicate to the client the language detected server-side
order: ["htmlTag"],
// Because we only use htmlTag, there's no reason to cache the language
// on the browser, so we disable it
caches: [],
},
});
startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<RemixBrowser />
</StrictMode>
</I18nextProvider>
);
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
}