🌩️

Cloudflare WorkersとD1でreact-router v7, drizzle-orm, valibot, conformメモ

に公開
2

備忘録メモ。

一旦動かすことができたのですが、しばらく別件に取り掛かるため、メモとして残します。

新規プロジェクト

$ 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

かえるかえる

良記事ありがとうございます。
ちょうどこの構成を模索していたので助かりました。