Remix+CloudflareでWebサイトを作る 37(actionのエラーハンドリング、Single Fetch、さよならremix-typedjson、Prismaのsetとconnect)
submission.reply()
で値を返してuseForm
に値を入れよう
【2024-10-18】actionでエラーが発生するとフォームがリセットされるのでthrowせずにconformとzodを使っているが、今までErrorBoundary()
を使ってこんな感じにしていた。
本当は Errorもパターン化して型をつけている。
問題なのはactionでエラーが発生するとフォームが完全にリセットされてしまうこと。
どうやらエラーが発生したときは、throw するのではなく、actionでsubmission.reply()
を返してそれを useForm
のlastResult
に入れる必要があるっぽい。
エラーが発生したらなんでもErrorBoundary()
でどうにかしてた。
export const action = async ({ request, context }: ActionFunctionArgs) => {
const formData = await request.clone().formData();
const submission = parseWithZod(formData, { schema });
if (submission.status !== "success") {
throw new Error(e);
// ✅ return { lastResult: submission.reply() } みたいな感じに
}
try {
await update(submission.value);
return json({ success: true });
} catch (e: unknown) {
throw new Error(e);
}
};
function Layout({ children }: { children: React.ReactNode }) {
const actionData = useActionData<typeof action>();
const [form, fields] = useForm({
// ✅ lastResult: actionData.lastResult ここを書く必要がある
constraint: getZodConstraint(schema),
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldValidate: "onInput",
defaultValue: {
name: actionData?.values?.name || sponsoredAd?.name,
},
});
return (
<div>
<div>title</div>
<Breadcrumb />
{children}
<div className="max-w-3xl mb-80">
<Form
method="POST"
id={form.id}
onSubmit={form.onSubmit}
className="space-y-6"
>
<div className="flex justify-end mt-10">
<Button type="submit">更新</Button>
</div>
</Form>
</div>
</div>
);
}
export default function Page() {
return (
<Layout>
<Outlet />
</Layout>
);
}
// 🚨 throw するとこっちに入ってくる
export function ErrorBoundary() {
const error = useRouteError();
return (
<Layout>
<ErrorMessage error={handleError(error)} />
<Outlet />
</Layout>
);
}
やりながら思ったけどErrorBoundary()
使わないようにするならlastResult
入れなくてもいいのか。
あと、SuspenseとAwait使ってる場合AwaitのerrorElementでloaderでthrowしたエラーをキャッチするという実装もできるな。
【2024-10-18】Remixをv2.13.0にアプデしてSingle Fetchに対応
溝口さんのツイートで思い出した。
remix-typedjson
のtypedjson
とかtypeddefer
使ってるから先にアプデしてSingle Fetchに対応しようかな。
Single Fetchとは
- クライアントサイドでのページ遷移が行われた際に、サーバーへの複数のHTTPリクエストを並行して行う代わりに、1つのHTTPリクエストを実行しまとめてレスポンスを返す機能
- いくつかの破壊的な変更があるが、大きな変更を加えることなく導入可能
- v2.9ではフィーチャーフラグとして提供されており、2.13.0で安定化、v3以降ではデフォルトの挙動になる
ベストプラクティスがわからなさすぎるので、一旦以下のような型を作ってactionで返すようにする。
import type { SubmissionResult } from "@conform-to/react";
type SuccessJson = {
success: true;
lastResult: SubmissionResult<string[]>;
};
export function successJson({
lastResult,
}: {
lastResult: SuccessJson["lastResult"];
}): SuccessJson {
return {
success: true,
lastResult,
};
}
type ErrorJson = {
success: false;
error: string;
lastResult: SubmissionResult<string[]>;
};
export function errorJson({
lastResult,
error,
}: {
lastResult: ErrorJson["lastResult"];
error: unknown;
}): ErrorJson {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
lastResult,
};
}
こんな感じで使う。
export const action = async ({ request, context }: ActionFunctionArgs) => {
const formData = await request.clone().formData();
const submission = parseWithZod(formData, { schema });
const lastResult = submission.reply();
if (submission.status !== "success") {
return errorJson({ error: "入力した情報が正しくありません", lastResult });
}
try {
await update(submission.value);
return successJson({ lastResult });
} catch (e: unknown) {
return errorJson({ error, lastResult });
}
};
Property 'xxx' does not exist on type 'unknown'
と出て useLoaderData
で型推論がされない
【2024-10-19】Single Fetchにしたら 以下のようなコードを書いたらuseLoaderData
でProperty 'user' does not exist on type 'unknown'
というエラーが出た。
redirect
のimport先がミスっていた。
デバッグにさほど時間かからなかったけど、Remixはこれ系のエラーに度々遭遇してしまうな。
import { useLoaderData } from "@remix-run/react";
- import { redirect, } from "remix-typedjson";
+ import { redirect } from '@remix-run/cloudflare';
import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
export async function loader({ request, context }: LoaderFunctionArgs) {
const userId = await getUserId(request);
if (!userId) {
return redirect("/");
}
const user = await getUserById(context.db, userId);
if (!user) {
return redirect("/");
}
return { user };
}
export default function AdminUsersIndexPage() {
const { user } = useLoaderData<typeof loader>(); // 🚨 ここ!
return (
<div>{user}</div>
);
}
Single Fetchに対応したのでremix-typedjson
とおさらば! 👋
やったーーー!
今までありがとう!🙏🏻
set
とconnect
の違い
【2024-10-19】Prismaの調べる
- connect: 追加する場合
- disconnect: 一部を削除する
- set: 新しいレコードへ置き換える
setを使う場合
- レコードのフィールドの値を直接変更したいとき
- ユーザーの名前、メールアドレス、プロフィールなどの変更
- 投稿のタイトル、内容の変更
- 既存の関連付けは維持したいとき
- 新しい値に置き換えるだけで、他の関連付けは変更しない
- 注意点:
- setで関連付けを変更しようとすると、既存の関連付けが切れてしまう可能性がある
connectを使う場合
- 既存のレコードと新しいレコードを関連付けたいとき
- 投稿に作者を割り当てる
- 注文に商品を紐づける
- 既存のレコードの関連付けを変更したいとき
- 投稿の作者を変更する
- 注意点:
- connectで指定したレコードは既に存在している必要がある。
- createと組み合わせて新しいレコードを作成しながら、既存のレコードと関連付けることもできる
実際に使う
わかった気になっているだけかもしれないので動かして確認する。
例として記事にA、Bという2つのタグが紐づいているとする。
この時、Bを削除してCを新たに付け加えるという操作を行う。
- 更新時にsetを使う
- ✅ Bは削除される
- ✅ Cが新たに付け加えられる
- 更新時にconenctを使う
- 🚨 Bは削除されない
- ✅ Cが新たに付け加えられる
connect, disconnectを使ってもできるのでは
setは一度すべての関連付けを削除し、新たに指定されたIDとのレコードとの関連を再構築している。
特定の関連付けを維持したい場合、disconnectで削除対象を絞り込み、connectで新しい関連付けを追加するらしい。
多対多の場合
これも動かしてわかったけど多対多の場合はsetを使って直接更新しようとするとエラーになる。
既存のレコードを直接更新するものなので適していない。
この場合はcreate
とdelete
を使う。