😄
Next.jsのApp RouterでTodoアプリを作る
Next.jsのApp RouterでTodoアプリを作る
App Routerを触ってなかったので、Todoアプリを作ることでApp Routerについて学習していこうと思います。useStateでタスクを管理するのではなく、実際にデータベースに書き込むTodoアプリを作っていこうと思います。
技術スタック
- Next.js(app)
- TailwindCSS
- Prisma
- PostgreSQL
- docker-compose
プロジェクトの開始
コマンドで一発
質問は適当にYES
npx create-next-app .
今回のディレクトリ構造
src
├ app
├ feature
├ todo
├ components // コンポーネント
├ db // drizzle関連
ライブラリのインストール
※eslint, prettierは好みで
npx shadcn@latest init
npm install drizzle-orm postgres
npm install -D drizzle-kit
drizzleのセットアップ
Drizzleの接続のメモ記事参照
使用するshadcnコンポーネント
npx shadcn@latest add alert-dialog button card input label
Todoアプリを作る
ダイアログコンポーネントの作成
削除するときの確認ダイアログを表示します。見栄えのために作っただけなので、いらない場合作る必要はありません
import { GeistSans } from "geist/font/sans";
import React, { ReactNode } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { cn } from "@/utils/cn";
export const DeleteTaskAlert = ({
children,
onClick,
}: {
children: ReactNode;
onClick: React.ComponentProps<"button">["onClick"];
}) => (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent className={cn(GeistSans.className)}>
<AlertDialogHeader>
<AlertDialogTitle>タスクを削除しますか?</AlertDialogTitle>
<AlertDialogDescription>
この操作は取り消せません
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>キャンセル</AlertDialogCancel>
<AlertDialogAction
className={cn("bg-red-500 hover:bg-red-600")}
onClick={onClick}
asChild
>
<Button>削除する</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
TodoForm
タスクを追加するフォームコンポーネントです。FormActionを使っています。これについてもまだ調べる必要あり。クライアント側で関数を作ることでformの値をリセットするにしました
"use client";
import { useRef } from "react";
import { addTask } from "../actions/submit";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/utils/cn";
export const TodoForm = () => {
const formRef = useRef<HTMLFormElement | null>(null);
const submitFunc = async (data: FormData) => {
await addTask(data);
if (formRef.current) {
formRef.current.reset();
}
};
return (
<form action={submitFunc} className="flex flex-col gap-2" ref={formRef}>
<Label>タスク名を記述してください</Label>
<Input
name="todo"
className={cn("focus:outline-sky-600 focus-visible:ring-sky-600")}
/>
<Button className="w-fit bg-sky-500 hover:bg-sky-600">
タスクを追加
</Button>
</form>
);
};
TodoList
親から渡されたtodosをmapを使ってリスト形式に表示。削除ボタンをクリックするとそのタスクを削除することができる
"use client";
import { deleteTodoFunc } from "../actions/delete";
import { DeleteTaskAlert } from "@/components/notifications/alert-dialog/delete-task";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export const TodoList = (props: {
todos: {
id: number;
title: string;
createdAt: Date;
completed: boolean;
updatedAt: Date;
}[];
}) => {
const { todos } = props;
return (
<div className="flex flex-col gap-4">
{todos.map((todo) => (
<Card key={todo.id}>
<CardHeader>
<CardTitle>{todo.title}</CardTitle>
<CardDescription>
{new Date(todo.createdAt).toLocaleDateString("sv-SE")}
</CardDescription>
</CardHeader>
<CardContent>
<form>
<DeleteTaskAlert onClick={() => deleteTodoFunc(todo.id)}>
<Button variant={"destructive"}>削除</Button>
</DeleteTaskAlert>
</form>
</CardContent>
</Card>
))}
</div>
);
};
データベース操作の関数
今回はタスクの追加と削除のみを実装。サーバーサイド側で処理を行う。
"use server";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { db } from "@/db/client";
import { todos } from "@/db/schema";
export const addTodo = async (data: FormData) => {
const todo = data.get("todo") as string;
try {
await db
.insert(todos)
.values({
title: todo,
completed: false,
})
.returning();
revalidatePath("/");
} catch (e) {
if (e instanceof Error) {
throw new Error(e.message);
}
}
};
export const deleteTodo = async (id: number) => {
try {
await db.delete(todos).where(eq(todos.id, id)).returning();
revalidatePath("/");
} catch (e) {
if (e instanceof Error) {
throw new Error(e.message);
}
}
};
addTask
タスクを追加する
"use server";
import { addTodo } from "./todo";
export const addTask = async (data: FormData) => {
const todo = data.get("todo") as string;
if (!todo) return;
if (!todo.length) return;
if (!todo.trim()) return;
await addTodo(data);
};
deleteTask
タスクを削除する
"use server";
import { deleteTodo } from "./todo";
export const deleteTodoFunc = async (id: number) => {
if (!id) return;
await deleteTodo(id);
};
ページ
- 今回はペライチアプリなのでapp/page.tsxのみ使用する。
- RSCでは直接データベースにアクセスることができるらしい?
import { TodoForm } from "./_feature/todo/components/todo-form";
import { TodoList } from "./_feature/todo/components/todo-list";
import { db } from "@/db/client";
import { todos as todosSchema } from "@/db/schema";
const Top = async () => {
const todos = await db.select().from(todosSchema).orderBy(todosSchema.id);
return (
<>
<main className="h-screen w-screen overflow-x-hidden overflow-y-scroll">
<div className="mx-auto flex w-4/5 flex-col gap-8 py-8 lg:w-2/5">
<h2 className="text-4xl font-bold">Todoアプリ</h2>
<TodoForm />
<TodoList todos={todos} />
</div>
</main>
</>
);
};
export default Top;
完成
汚いコードではあるがとりあえず動作するものは作れた。axiosでapiルートに通信せずとも直接データベースを操作できるのはとても面白い
参考記事
最後に
間違っていることがあればコメントに書いていただけると幸いです。
よろしくお願いいたします。
Discussion