🐡

【NextJs14】NextJs14 と 便利なライブラリ【#32Delete Todo Server Actions】

2024/02/16に公開

【#32Delete Todo Server Actions】

YouTube: https://youtu.be/Sag4KlWUxwQ

https://youtu.be/Sag4KlWUxwQ

今回はTodoの削除ボタンを実装します。

まずはボタンをクライアントサイドのコンポーネントに移動します。

app/(main)/account/_components/todo-actions.tsx
"use client";

import { useTransition } from "react";
import { toast } from "sonner";
import { CheckCircle, Trash2 } from "lucide-react";

import { deleteTodo } from "@/actions/todo-delete";
import { Button } from "@/components/ui/button";

interface TodoActionsProps {
  todoId: string;
  isCompleted: boolean;
}

export const TodoActions = ({ todoId, isCompleted }: TodoActionsProps) => {
  const [isPending, startTransition] = useTransition();

  const onDelete = () => {
    startTransition(() => {
      deleteTodo({ id: todoId })
        .then((data) => toast.success(data.success))
        .catch((data) => {
          if (data.error) {
            toast.error(data.error);
          } else {
            toast.error("Something went wrong!");
          }
        });
    });
  };

  return (
    <div className="flex items-center gap-x-2">
      <Button variant={isCompleted ? "Completed" : "secondary"}>
        <CheckCircle className="h-5 w-5" />
      </Button>
      <Button onClick={onDelete} disabled={isPending} variant="destructive">
        <Trash2 className="h-5 w-5" />
      </Button>
    </div>
  );
};
app/(main)/account/page.tsx
import { formatDistanceToNow } from "date-fns";

import { db } from "@/lib/db";

import { Separator } from "@/components/ui/separator";

import { TodoForm } from "./_components/todo-form";
import { TodoActions } from "./_components/todo-actions";

const getTodos = async () => {
  const todos = await db.todo.findMany({
    orderBy: {
      createdAt: "asc",
    },
  });
  return todos;
};

const AccountPage = async () => {
  const todos = await getTodos();

  return (
    <div className="p-6 w-full h-full">
      <div className="w-full h-full flex flex-col gap-y-3">
        <div className="flex flex-col md:flex-row items-center justify-between gap-2">
          <h2 className="text-center md:text-left text-3xl font-bold">Todos</h2>
          <TodoForm />
        </div>
        <Separator />
        <ul className="w-full space-y-3">
          {todos.map((todo, idx) => (
            <li
              key={todo.id}
              className="flex flex-col md:flex-row items-center justify-between gap-x-4 gap-y-2"
            >
              <div className="flex flex-col md:flex-row flex-1 items-center justify-between gap-x-3">
                <p className="text-xl font-semibold">
                  <span className="pr-2">{idx + 1}</span>
                  <span className={todo.isCompleted ? "line-through" : ""}>
                    {todo.title}
                  </span>
                </p>
                <p>
                  {formatDistanceToNow(todo.createdAt, { addSuffix: true })}
                </p>
              </div>
              <TodoActions todoId={todo.id} isCompleted={todo.isCompleted} />
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default AccountPage;
types/schema.ts
import * as z from "zod";

export const CreateTodoSchema = z.object({
  title: z.string().min(1, {
    message: "Title is required.",
  }),
});

export const DeleteTodoSchema = z.object({
  id: z.string({
    required_error: "Id is required",
    invalid_type_error: "Id is required",
  }),
});
actions/todo-delete.ts
"use server";
import * as z from "zod";
import { DeleteTodoSchema } from "@/types/schema";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
import { auth } from "@clerk/nextjs";

export const deleteTodo = async (values: z.infer<typeof DeleteTodoSchema>) => {
  const { userId } = auth();

  if (!userId) {
    return {
      error: "Unauthorized",
    };
  }

  const validatedFields = DeleteTodoSchema.safeParse(values);

  if (!validatedFields.success) {
    return {
      error: "Invalid fields",
    };
  }

  const { id } = validatedFields.data;

  await db.todo.delete({
    where: {
      id,
    },
  });

  revalidatePath("/account");

  return { success: "Todo deleted!" };
};

idの渡し方につきましては、
ボタンをフォームでラップして、インプットタグのvalueにidを設定する方法や、
以下のドキュメントのような方法があります。

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#passing-additional-arguments

Discussion