Next.js(App Router) + Zod + Prisma+ React Hook FormでTODOリストを作りました

# 背景
React Hook Formの良さを体感すべく、React Hook Formを使った時と使わなかった時のTODOリストを実装します。

当記事は、React Hook Formを使った場合の実装します。
GitHubは[こちら](https://github.com/asami-okina/todo-list/tree/main/todo-list-next-and-react-hook-form)

# 1. プロジェクトのセットアップ
```bash
npx create-next-app@latest todo-list-next-and-react-hook-form
cd todo-list-next-and-react-hook-form
```
プロンプトでは以下のように選択してください:

- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- src/ directory: Yes
- App Router: Yes
- Import alias: No

# 2. 必要なパッケージをインストール
```bash
npm install @prisma/client zod
npm install -D prisma
npm install react-hook-form @hookform/resolvers zod
```

# 3. Docker Compose ファイルの作成
プロジェクトのルートにdocker-compose.ymlファイルを作成します。

```txt:docker-compose.yml
version: '3.8'
services:
  db:
    image: postgres:13
    environment:
      POSTGRES_USER: todouser
      POSTGRES_PASSWORD: todopassword
      POSTGRES_DB: todo_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
```

# 4. PostgreSQL の起動
```bash
docker-compose up -d
```

# 5. Prismaの初期化
```bash
npx prisma init
```

# 6. .envファイルを編集
```bash
DATABASE_URL="postgresql://todouser:todopassword@localhost:5432/todo_db?schema=public"
```

# 7. prisma/schema.prismaファイルを編集
```txt:prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Todo {
  id        Int      @id @default(autoincrement())
  title     String
  completed Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
```

# 8. データベースのマイグレーションを実行
```bash
npx prisma migrate dev --name init
```

# 9. next.config.jsファイルを編集してサーバーアクションを有効化

```js:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
}

module.exports = nextConfig
```

# 10. コードを書く
[GitHub](https://github.com/asami-okina/todo-list/tree/main/todo-list-next-and-react-hook-form)参照

それでは、React Hook Formを使わなかった時の実装と比較して違うところを書いていきます。
### 1. Form用のSchemaが追加される
```ts
import { z } from "zod";

export const TodoSchema = z.object({
  id: z.number().optional(),
  title: z
    .string()
    .min(1, "Title is required")
    .max(100, "Title must be 100 characters or less"),
  completed: z.boolean().default(false),
});

// 追加された
export const TodoFormSchema = TodoSchema.omit({ id: true, completed: true });

export type Todo = z.infer<typeof TodoSchema>;
// 追加された
export type TodoFormData = z.infer<typeof TodoFormSchema>;
```

### 2. React Hook Formを使用してフォームが実装される
```ts
"use client";

import { addTodo } from "@/app/actions";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { TodoFormSchema, TodoFormData } from "@/lib/validate";

export function AddTodoForm() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<TodoFormData>({
    resolver: zodResolver(TodoFormSchema),
  });

  const onSubmit = async (data: TodoFormData) => {
    await addTodo(data);
    reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex gap-2">
      <input
        {...register("title")}
        placeholder="New todo"
        className="flex-grow px-2 py-1 border rounded text-gray-800"
      />
      <button
        type="submit"
        className="px-4 py-1 bg-blue-500 text-white rounded"
      >
        Add
      </button>
      {errors.title && (
        <p className="text-red-500 text-sm">{errors.title.message}</p>
      )}
    </form>
  );
}
```

# 11. アプリケーションの起動
```bash
npm run dev
```

# 補足
開発が終わったら、Dockerコンテナを停止します。
## 1. Dockerコンテナの停止
Dockerコンテナを停止したい場合は、以下のコマンドを使用します。
```bash
docker-compose down
```
## 2. Dockerコンテナの停止して、データを完全に削除したい場合
-vオプションを追加します
```bash
docker-compose down -v
```

# まとめ
私が体感したReact Hook Formを使った時と使わなかった時の違いは、以下２つでした。
- React Hook Formは入力中や送信前にzodのバリデーションを行ってくれる
- React Hook Formはエラーの状態管理が自動的に行われる