🌩️
Cloudflare WorkersとD1でreact-router v7, drizzle-orm, valibot, conformメモ
備忘録メモ。
一旦動かすことができたのですが、しばらく別件に取り掛かるため、メモとして残します。
新規プロジェクト
$ npx create-react-router@latest --template remix-run/react-router-templates/cloudflare-d1
(中略)
create-react-router v7.6.0
dir Where should we create your new project?
./example_react-router_cloudflare-workers
◼ Template: Using remix-run/react-router-templates/cloudflare-d1...
✔ Template copied
git Initialize a new git repository?
Yes
deps Install dependencies with npm?
No
(中略)
$ cd ./example_react-router_cloudflare-workers
ncuを使ってnpmをアップデートしておく(drizzle-ormを最新にする)
$ ncu -u
$ npm install
DBを作成
Cloudflare D1にDBを作成
ここではexample_react-router_dbという名前で作成します
$ npx wrangler d1 create example_react-router_db
表示されたUUIDとDB名をwrangler.jsoncに記述
wrangler.jsonc
// 書き換える
{
"d1_databases": [
{
"binding": "DB",
"database_name": "example_react-router_db",
"database_id": <ここにUUID>,
"migrations_dir": "drizzle"
}
]
}
テーブルを作成
database/schema.ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
// 商品テーブル
export const items = sqliteTable('items', {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(), // 商品名
price: integer().notNull(), // 価格
});
デフォルトのマイグレーションを一旦削除します
$ rm -r drizzle
database/schema.tsの内容からマイグレーションを作成し、ローカルのDBに反映させます
$ npx drizzle-kit generate
$ npx wrangler d1 migrations apply --local <DB名>
マイグレーションの一覧を確認
$ npx wrangler d1 migrations list --local <DB名>
valibot, conform
valibotとconformを入れます。drizzleとの連携も入れます。
多分だけど、現時点でERESOLVEエラーが出てしまうので--forceオプションをつけてます。
$ npm i valibot drizzle-valibot @conform-to/react @conform-to/valibot --force // --forceオプションはやむを得ず
shadcn/ui
tsconfig.json
// 追加
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"]
},
tsconfig.cloudflare.json
// 変更
"paths": {
"@/database/*": ["./database/*"],
"@/*": ["./app/*"]
},
$ npx shadcn@latest init
$ npx shadcn@latest add button
$ npx shadcn@latest add input
$ npx shadcn@latest add card
フォーム画面実装
app/root.tsx
// 〜
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<main className="container mx-auto">{children}</main>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
// 〜
app/routes/home.tsx
import type { Route } from './+types/home';
import * as schema from '@/database/schema';
import * as v from 'valibot';
import { createInsertSchema } from 'drizzle-valibot';
import { parseWithValibot } from '@conform-to/valibot';
import { Form, useSubmit, useNavigation } from 'react-router';
import { getFormProps, getInputProps, useForm } from '@conform-to/react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
export function meta({}: Route.MetaArgs) {
return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
}
const itemInsertSchema = createInsertSchema(schema.items, {
// 商品名は100文字まで
name: (schema) => v.pipe(schema, v.string(), v.maxLength(100)),
// 価格は9,999,999円まで
price: (schema) => v.pipe(schema, v.integer(), v.maxValue(9999999)),
});
export async function action({ request, context }: Route.ActionArgs) {
const formData = await request.formData();
//console.log(formData);
const submission = parseWithValibot(formData, { schema: itemInsertSchema });
//console.log(submission.reply());
if (submission.status !== 'success') {
return Response.json(submission.reply());
}
await context.db.insert(schema.items).values(submission.value);
return Response.json(submission.reply());
}
export async function loader({ context }: Route.LoaderArgs) {
const items = await context.db.query.items.findMany();
return { items };
}
export default function Home({ actionData, loaderData }: Route.ComponentProps) {
const navigation = useNavigation();
const submit = useSubmit();
const [form, fields] = useForm({
shouldValidate: 'onBlur',
lastResult: actionData,
onValidate({ formData }) {
return parseWithValibot(formData, { schema: itemInsertSchema });
},
onSubmit(event) {
// 送信後にFormをクリアする
event.preventDefault();
submit(event.currentTarget);
event.currentTarget.reset();
},
});
return (
<>
<Card className="mb-4">
<CardHeader>
<CardTitle>商品登録</CardTitle>
</CardHeader>
<CardContent>
<Form method="post" {...getFormProps(form)}>
<div className="mb-4">
<Input {...getInputProps(fields.name, { type: 'text' })} placeholder="商品名" required />
{!fields.name.valid ? (
<p id={fields.name.errorId} className="text-sm text-red-600 mt-2">
{fields.name.errors}
</p>
) : (
<></>
)}
</div>
<div className="mb-4">
<Input {...getInputProps(fields.price, { type: 'text' })} placeholder="価格" required />
{!fields.price.valid ? (
<p id={fields.name.errorId} className="text-sm text-red-600 mt-2">
{fields.price.errors}
</p>
) : (
<></>
)}
</div>
<Button type="submit" disabled={navigation.state === 'submitting'}>
登録
</Button>
</Form>
</CardContent>
</Card>
{loaderData.items.map(({ id, name, price }) => (
<Card key={id} className="mb-4">
<CardContent>
{name} 価格: {price}円
</CardContent>
</Card>
))}
</>
);
}
$ npm run dev
// $ react-router dev
デプロイ
Cloudflare D1にマイグレーションを実行
$ npx wrangler d1 migrations apply --remote <DB名>
ビルドしてデプロイ
$ npm run build
$ npx wrangler deploy
Discussion
良記事ありがとうございます。
ちょうどこの構成を模索していたので助かりました。
コメントありがとうございます。お役に立てたようでよかったです。