0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Remixを学びたい(Step4:DBとの疎通をする)

0
Last updated at Posted at 2025-04-29

はじめに

前回までの続きです!
前回まではMockで対応してましたが、今回はSQLiteのDBにデータを立て、CRUD処理を実施します!!

シリーズもの

成果物

step4.gif
→SQLiteでDBにデータを保存し、CRUD処理を実装しています!

実装

# Prismaのインストール
npm install -D prisma
npm install @prisma/client
# Prismaの初期化(SQLite使用)
npx prisma init --datasource-provider sqlite

→このコマンドを打つと、prisma/schema.prismaが作成されます

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

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

env
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="file:./dev.db"
migrate実行
~/develop/remix_sample  (feat/step4_db_crud)$ npx prisma migrate dev --name init

ソースコード

ディレクトリ構成
~/develop/remix_sample  (feat/step4_db_crud)$ tree app/
app/
├── actions
│   └── todoActions.ts 
├── components
│   └── todo
│       ├── TodoForm.tsx
│       ├── TodoItem.tsx
│       └── TodoList.tsx
├── entry.client.tsx
├── entry.server.tsx
├── lib
│   └── db.server.ts # SQLiteの接続Client
├── models
│   └── todo.ts
├── root.tsx
├── routes
│   └── _index.tsx
├── services
│   └── todoService.ts
└── tailwind.css

8 directories, 12 files
app/routes/_index.tsx
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { useLoaderData, useNavigation } from "@remix-run/react";
import type { MetaFunction } from "@remix-run/node";
import { addTodo, deleteTodo, getTodos, toggleTodo } from "~/services/todoService";
import { TodoForm } from "~/components/todo/TodoForm";
import { TodoList } from "~/components/todo/TodoList";
import type { Todo as PrismaTodo } from "@prisma/client";

export type Todo = Omit<PrismaTodo, "createdAt" | "updatedAt"> & {
	createdAt: string;
	updatedAt: string;
};

export const meta: MetaFunction = () => {
	return [
		{ title: "TODO App" },
		{ name: "description", content: "Simple TODO App with Remix" },
	];
};

// データを取得(サーバーサイド)
export async function loader() {
	const todos = await getTodos();
	return json({ todos });
}

// データを更新(サーバーサイド)
export async function action({ request }: ActionFunctionArgs) {
	const formData = await request.formData();
	const intent = formData.get("intent");

	try {
		switch (intent) {
			case "add": {
				const title = formData.get("title");
				if (typeof title !== "string" || !title) {
					return json({ error: "Title is required" }, { status: 400 });
				}
				await addTodo(title);
				break;
			}
			case "toggle": {
				const id = formData.get("id");
				if (typeof id === "string") {
					await toggleTodo(Number.parseInt(id));
				}
				break;
			}
			case "delete": {
				const id = formData.get("id");
				if (typeof id === "string") {
					await deleteTodo(Number.parseInt(id));
				}
				break;
			}
			default: {
				return json({ error: "Invalid intent" }, { status: 400 });
			}
		}
		return json({ ok: true });
	} catch (error) {
		console.error("Database error:", error);
		return json({ error: "データベースエラーが発生しました" }, { status: 500 });
	}
}

export default function Index() {
	const { todos } = useLoaderData<typeof loader>();
	const navigation = useNavigation();
	const isLoading = navigation.state === "loading";

	return (
		<div className="max-w-2xl mx-auto p-4">
			<h1 className="text-2xl font-bold mb-4">TODO App</h1>

			{isLoading ? (
				<div className="text-center text-gray-500 my-4">Loading...</div>
			) : (
				<>
					<TodoForm />
					<TodoList todos={todos} />
				</>
			)}
		</div>
	);
}

app/services/todoService.ts
import { db } from "~/lib/db.server";

export async function getTodos() {
	return db.todo.findMany({
		orderBy: { createdAt: "desc" },
	});
}

export async function addTodo(title: string) {
	return db.todo.create({
		data: {
			title,
			completed: false,
		},
	});
}

export async function toggleTodo(id: number) {
	const todo = await db.todo.findUnique({ where: { id } });
	if (!todo) return null;

	return db.todo.update({
		where: { id },
		data: { completed: !todo.completed },
	});
}

export async function deleteTodo(id: number) {
	return db.todo.delete({
		where: { id },
	});
}

app/actions/todoActions.ts
import { json } from "@remix-run/node";
import { addTodo, toggleTodo, deleteTodo } from "~/services/todoService";

export async function handleAddTodo(formData: FormData) {
	const title = formData.get("title");
	if (typeof title !== "string" || !title) {
		return json({ error: "Title is required" }, { status: 400 });
	}
	await addTodo(title);
	return json({ ok: true });
}

export async function handleToggleTodo(formData: FormData) {
	const id = formData.get("id");
	if (typeof id === "string") {
		await toggleTodo(Number.parseInt(id));
	}
	return json({ ok: true });
}

export async function handleDeleteTodo(formData: FormData) {
	const id = formData.get("id");
	if (typeof id === "string") {
		await deleteTodo(Number.parseInt(id));
	}
	return json({ ok: true });
}
app/lib/db.server.ts
import { PrismaClient } from "@prisma/client";

let db: PrismaClient;

declare global {
	// eslint-disable-next-line no-var
	var __db: PrismaClient | undefined;
}

if (process.env.NODE_ENV === "production") {
	db = new PrismaClient();
} else {
	if (!global.__db) {
		global.__db = new PrismaClient();
	}
	db = global.__db;
}

export { db };

app/models/todo.ts
import type { Todo as PrismaTodo } from "@prisma/client";

// Prismaの型定義を再利用
export type Todo = PrismaTodo;

さいごに

いかがでしたか?Remixがなんとなく理解できてきましたかね??
まだまだ甘噛み程度なので、これからプロダクトを作ってもっと深く理解していこうと思います!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?