Remix+CloudflareでWebサイトを作る 3(request.clone()・StrictMode・Conform・DarkMode)
【2024-02-04】エラー: Your worker created multiple branches of a single stream...
エラー内容
✘ [ERROR] Your worker created multiple branches of a single stream (for instance, by calling `response.clone()` or `request.clone()`) but did not read the body of both branches. This is wasteful, as it forces the system to buffer the entire stream of data in memory, rather than streaming it through. This may cause your worker to be unexpectedly terminated for going over the memory limit. If you only meant to copy the request or response headers and metadata (e.g. in order to be able to modify them), use the appropriate constructors instead (for instance, `new Response(response.body, response)`, `new Request(request)`, etc).
actionを実行すると中身が空っぽでもこのエラーが出る。
issueを見る限りやはりWrangler関連っぽくて今も治ってないとのこと。一旦は無視して進める。
【2024-02-04】エラー: TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.
エラー内容
ログイン処理を行うために remix-hook-form と remix-auth を使って以下のようなコードを書いていたところエラーが発生
export const action = async ({ request, context }: ActionFunctionArgs) => {
try {
const { errors, data } = await getValidatedFormData<FormData>(
request,
resolver
);
if (errors) {
return new AuthError("ログイン情報が正しくありません");
}
return await authenticator.authenticate("user-login", request, {
context: { context, data },
successRedirect: "/admin",
});
} catch (e) {
return new AuthError("ログイン情報が正しくありません");
}
};
TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.
原因・解決方法
RemixはformDataを一回しか呼び出せないようになっているため、action内で呼び出してしまうとRemix Authが動かなくなってしまいます
と書いてある。
request.clone()
を使って解決
export const action = async ({ request, context }: ActionFunctionArgs) => {
try {
const { errors, data } = await getValidatedFormData<FormData>(
request.clone(),
【2024-02-05】サーバーからのResponse HeadersにSet-Cookieが入っているのにブラウザにCookieが入らない
背景
remix-auth を使ってメアドとパスワードで管理画面にログインできるようにしたい
サーバーからSet-Cookieを含んだHedaerを返しているのになかなかCookieに値が入らない。
import {
createCookie,
createCookieSessionStorage,
redirect,
} from "@remix-run/cloudflare";
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
sameSite: "lax",
path: "/",
httpOnly: true,
secrets: ["a"],
secure: true, //process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24,
},
});
async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return sessionStorage.getSession(cookie);
}
export async function createUserSession({
request,
redirectTo,
userId,
remember,
}: {
request: Request;
redirectTo: string;
userId: number;
remember: boolean;
}) {
const session = await getSession(request);
session.set(USER_SESSION_KEY, userId);
const headers = {
"Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: remember
? 60 * 60 * 24 * 7 // 7 days
: undefined,
}),
};
return redirect(redirectTo, { headers });
}
原因・解決方法
コードは多分おかしくない気がしていて何がだめなのか良くわからない...。
なんとなくCopilotに聞いてみた。
Me: サーバーからのReponse HeadersにSet-Cookieがあるのですが、ブラウザのCookieには値が入っていません。考えられる原因をいくつか教えてください。
Copilot:
ブラウザがサーバーからのSet-Cookieヘッダーを無視する、つまりクッキーがブラウザに保存されない原因はいくつか考えられます:
1. Secure属性: Secure属性が設定されている場合、そのクッキーはHTTPS接続を通じてのみ送信されます。HTTP接続では無視されます。
...
この時点ではっとした。
process.env
を使えるようにする前にこの開発をはじめてしまって適当に secure: true
という値を入れてしまっていた。
はぁ。
【2024-02-06】ReactのStrictModeとはなんなのか
経緯
RemixのReceStackやSuperflareのExample codeを見た時にある <React.StrictMode></React.StrictMode>
とはなんなのか気になったので調べてみる。
調べてまとめてみる
ざっくり要点を書くと...
どう使う?
- 開発環境においてコンポーネントの一般的なバグを早期に見つけるのに役立つ
- というのも関数が純粋であれば、結果は毎回同じになるはずで2回実行しても振る舞いが変わらないはずであるから
- 具体的には二重レンダーをすることでバグを見つけることが可能になる
- React18では、コンポーネントが表示された瞬間にそれを1度破棄して、またすぐ表示するということを裏側で行っている
特徴
- アプリ全体への適用もできれば、一部への適用も可能
- StrictModeは開発環境でのみ適用され、本番環境では1回しかレンダリングしない👍🏻
どんな背景がある?
- Reactは将来的にstateを保ったままUIの一部を追加・削除する機能を導入したい
- 具体的にはoffscreenという機能が追加されると言われている
- 2024-03-01追記:React 19 になってOffscreen来た!
- タブ遷移のように他のタブに行ってから戻ってきた場合にstateを復元させることができる
- 具体的にはoffscreenという機能が追加されると言われている
- これによってパフォーマンスは向上するものの、副作用も増えてバグの温床ができる
- StrictModeを導入することで開発時にバグに気づきやすくしておこう
参考
【2024-02-06】Hello, Conform!
Conformというフォームライブラリ
つい一昨日上記スクラップで「Remix Hook Form良さそうやん」と言って導入したわけだが、色々調べているうちにConformというライブラリを発見した。
ざっくりいうと「バリデーションはフロントエンド、バックエンド両方で行うが、それを簡単にできるやーつ」というもの。
全然関係ないけど、やっぱ年末年始ってダウンロード数減るのね。
ref: @conform-to/react - npm Package Security Analysis - Socket
使ってみる
ユーザー新規追加フォームは remix-hook-form を使ったので、タグ新規作成フォームにConformを使ってみる。
loader
, action
で parseWithZod
を書けるところが嬉しい。
shouldValidate
ではバリデーションのタイミングを onInput
onBlur
onSubmit
の3つから選択できる。
export const action = async ({ request, context }: ActionFunctionArgs) => {
const formData = await request.clone().formData();
const submission = parseWithZod(formData, { schema });
...
}
export default function PageName() {
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
lastResult,
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldValidate: "onInput",
});
}
UI側の書き方もたいして変わらないし、parseWithZod
嬉しいしConformを使ってみようかなと思う。
その他
Conformを紹介している記事のここに以下のような文言がある。
Conformではzodの他にyupも可能です。しかし、zodを使うとJavaScriptのサイズが大きくなってしまうのでzodよりvalibotを使いたいって方もいるのではないでしょうか?かくゆう私自身もそうです。
JavaScriptのサイズの大きさまでもを気にしてライブラリ選定をしようという意識が全然無かったな...。
そういうのも見なければ、と思った。
【2024-02-06】Cloudflare D1 + Prisma(SQLite)って使えるんだっけ?
背景
色々ぐぐっていくうちにこのリポジトリを見つけた。
cloneしてみるとPrismaを使っている...!?
最終更新日が2年前だけどCloudflare D1とPrismaってその時点で使えるのか?
調べてみよう。
今の開発状況って?
この2022-03-12にOpenしたissueを見ると、10ヶ月たった今でもPrismaが使えるようにはなってないぽいな。
【2024-02-07】Middlewareでログインしているか確認したい
やりたいこと
例えば /admin
にアクセスした時にログインしていない場合は /signin
に飛ばす、的なやつを実装したい。
その他にもログインが必要なaction の前にはログインしているかどうかを確認して、ログインしていなければ/signin
にリダイレクトさせたい。
どうやんの?
結論
現段階ではなさそう
Discussionsを発見
ここの「Require Authentication」の箇所はまさにやりたいこと
import { type Middleware } from "@remix-run/react";
export function requireUser({ session }: Middleware) {
let user = session.get("user");
if (!user) {
session.set("returnTo", request.url);
throw redirect("/login", 302);
}
}
issueを発見
loader内で毎度関数を呼びだせという回答があるがさすがに無理がある。
画面数はおそらく20くらいでめちゃくちゃに多いわけじゃないし一旦ベタ書きしとこうかな。
早く公式で使えるようになると嬉しい。
【2022-02-07】MUIのJoyUIでLightMode/DarkModeに対応する
やろう
知っているぞ。こういうのは早めにやったほうが良いということを。
そういや
Joy UIのテンプレートコードの中に <CssBaseline />
ってあったけどこれなんぞ。
- MUI が用意しているリセットCSS
- 前提としてブラウザが用意しているデフォルトCSSというものがある
- これはブラウザごとに微妙な差異がある
- これを調整する目的で使用される
へぇ。こいつはルートにあれば良さそう。
できた
これをルートに書いたら全部に適用される。
別に早めにやらなくても良いくらい簡単。
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<CssVarsProvider defaultMode="dark" disableTransitionOnChange>
<CssBaseline />
<Outlet />
</CssVarsProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}