Open6

Remix+CloudflareでWebサイトを作る 44(Requestの型エラー)

saneatsusaneatsu

【2024-11-23】load-context.tsRequestの型をinterface Requestではなくinterface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>にしたい

背景

Cloudflare PagesからCloudflare Workersに移行中。
load-context.ts をこのように書いた。

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も修正も効いたのか??
わからんが治ってしまった。

package.json
  "devDependencies": {
-   "@cloudflare/workers-types": "^4.20240925.0",
+   "@cloudflare/workers-types": "^4.20241112.0",
-   "wrangler": "3.87.0"
+   "wrangler": "^3.78.12"
  },
tsconfig.json
  "compilerOptions": {
    "types": [
      "@remix-run/cloudflare",
      "vite/client",
+     "@cloudflare/workers-types/2023-07-01"
    ],
saneatsusaneatsu

【2024-11-25】最近見た良かった記事をメモ

Cloudflare

https://zenn.dev/msy/articles/4c48d9d9e06147
https://zenn.dev/resistance_gowy/scraps/8ae6d2727496d4
https://blog.lacolaco.net/posts/cloudflare-image-transform-for-image-optimization/
https://dev.classmethod.jp/articles/remix-on-cloudflare-workers-w-kv/

SPA, SSR, SSG

https://qiita.com/manabito76/items/fe91eefe11a74dcf5126
https://zenn.dev/akino/articles/78479998efef55
https://blog.ecbeing.tech/entry/2024/03/25/080000
https://zenn.dev/pyteyon/scraps/e3e53012c7d3c9

React

https://zenn.dev/luvmini511/articles/71f65df05716ca
https://qiita.com/ryosuketter/items/1ebf2d68ba3317db53a9
https://qiita.com/odendayoko/items/e1c5d3b2abdaa02cbea0

Drizzle

https://zenn.dev/steg/articles/a6299112c1f1fd
https://zenn.dev/toridori/articles/9c41d1fd7f6a85
https://nexunity.tech/post/drizzle-indexes-constraints/
https://thinkami.hatenablog.com/entry/2024/05/02/203858#複合主キーあり
https://qiita.com/RuruCun/items/d3a3af59631752c068aa

https://orm.drizzle.team/docs/column-types/sqlite
https://orm.drizzle.team/docs/rqb
https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/sqlite-core/README.md
https://github.com/vahid-nejad/drizzle-orm-course/tree/main/src/db

キャッシュ

https://zenn.dev/mr_ozin/articles/6d48e17d16d12b
https://blog.stin.ink/articles/pokemon-soundlibrary-cache-strategy
https://www.seohacks.net/blog/24056/#:~:text=304 Not Modifiedとは、Webサーバーから送られる,ないことを示します。
https://zenn.dev/link/comments/19aeafd3c2a40a

saneatsusaneatsu

【2024-11-28】loaderの値を<Suspence>で表示しようとするとエラーになる

エラー

<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.
saneatsusaneatsu

原因を探る

https://github.com/saneatsu/remix-on-cf-worker

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

ほぼ同じだけどな??
全然わからん...。

もはやデバッグ用リポジトリをベースに、元のコードを追加していくほうが早い気がしてきたぞ。

saneatsusaneatsu

解決方法

1つずつ差分確認していったけど以下が原因だった。
発見むっず〜。

package.json
- "remix-i18next": "^6.4.1",
+ "remix-i18next": "^7.0.0",

その他

next-themesを使っている箇所でmouted &&にしているとサーバーでもConsoleでもエラーがでないが画面が真っ白のまま何も表示されないので外した。

ThemeProvider.tsx
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>;
}
saneatsusaneatsu

エラー: TypeError: Cannot convert undefined or null to object

remix-i18next のバージョン変更に伴いentry.client.tsentry.server.tsでエラーがでている。

entry.client.ts

このエラーが発生するとホットリロードが実行されない。

entry.client.tsx:52 TypeError: Cannot convert undefined or null to object
    at Function.values (<anonymous>)
    at getInitialNamespaces (

Before

entry.client.tsx
/**
 * 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

https://remix.run/resources/remix-i18next
ここに書いてあるコードに変える

entry.client.tsx
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);
}