1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js + TypescriptでミニCMSを作る(6. 記事の削除ボタン)

Posted at

プライベートが忙しくなっていまい、更新頻度が空いてしまいましたね。
とりあえず書き溜めていた記事だけでも、投稿がんばります!

削除ボタンを作成します

前回の続きです。

レビュー一覧_削除ボタン.png

削除ボタンの機能を実装します。

クライアントコンポーネントの特徴を利用する

今のコンポーネントの構造を再掲します。
Component-Structure-ReviewList.png

ReviewCardContainerというクライアントコンポーネントが、レビュー記事の配列を親コンポーネントから受け取ってそのまま表示している状態です。
この状態でDB上のレビューデータを削除したとしても、親から渡されるPropsはページが更新でもされてサーバに再要求されない限りは、更新されずに古いデータのままになってしまいます。
削除ボタンを押すたびに更新処理をかけるのはUXの質の低下に繋がりかねません。

そこで、クライアントコンポーネントでのみ利用できる、ReactフックuseStateの出番です。

state フック
state を使うと、ユーザの入力などの情報をコンポーネントに「記憶」させることができます。例えば、フォームコンポーネントは入力された文字を保持し、画像ギャラリのコンポーネントは選択された画像を保持できます。

これを使って、Propsで渡ってきたレビュー記事の配列をStateに記憶させ、削除ボタンが押されたときはDBへの削除命令とsetStateの2つの操作を同時に行うことにします。

State操作

各コンポーネントを以下の通り更新します。

components/ReviewCardContainer/ReviewCardContainer.tsx
'use client'

import { ReviewWithProductId } from "@/app/types";
import styles from "./CardContainer.module.css";
import ReviewCard from "@/app/components/ReviewCard/ReviewCard";
+ import { useState } from "react";

type Props = {
  title: string;
  reviews: ReviewWithProductId[];
}

export default function ReviewCardContainer(props: Props) {

-  const reviews = props.reviews;
+  const [reviews, setReviews] = useState(props.reviews);

+  const handleDelete = async (review : ReviewWithProductId) => {
+    const answer = confirm(`レビュー記事「${review.title}」を削除しますか?`);
+    if(answer){
+        //update reviews
+        const newReviews = reviews.filter((e) =>  e.id !== review.id )
+        setReviews(
+          newReviews
+        )
+    }
+  }

  
  return ( 

    <div>
      <hr />
      <div className={styles.titleContainer}>
        <h2>{props.title}</h2>
        <p>{reviews.length}</p>
      </div>
      {reviews.length == 0 
      ?  (<p className={styles.noPost}>投稿はありません。</p>) 
      :  reviews.map((review, index) =>(
-          <ReviewCard key={index} review={review} />
+          <ReviewCard key={index} review={review} onDeleteReview={(review) => handleDelete(review)} />
        ))}
    </div>
  )
}
components/ReviewCard/ReviewCard.tsx
import Image from "next/image";
import styles from "./reviewCard.module.css";
import Link from "next/link";
import { ReviewWithProductId } from "@/app/types";

interface Props {
  review: ReviewWithProductId,
+  onDeleteReview: (review: ReviewWithProductId) => void,
}

- export default function ReviewCard({review} : Props) {
+ export default function ReviewCard({review, onDeleteReview } : Props) {

    return (

    //中略
    
                <div className={styles.buttonContainer}>
                    <Link href={`/dashboard/review/${review.id}`} className={styles.buttonLink}>
                        <Image src="/img/icon/awesome-edit.svg" width={20} height={20} alt="編集"/>
                    </Link>

-                   <button className={styles.deleteButton}>
+                   <button onClick={() => onDeleteReview(review)} className={styles.deleteButton}>
                        <Image src="/img/icon/material-delete.svg" width={20} height={20} alt="削除"/>
                    </button>

                </div>
    //中略
    )
}

これで、ReviewCardコンポーネント上のゴミ箱ボタンが押されると、親のReviewCardContainerからPropsとして渡されたonDeleteReview関数が呼び出されます。
引数となったreviewは配列からfilter()関数を使って除去され、新しい配列がStateにセットされるというわけです。

この段階で、実際にNextのdev環境で削除ボタンを押すと、レビューが消えるのが確認できます。

image.png

しかし、DBには削除命令を送っていないので、DB上にはデータが残ったままです。ページを更新すれば、ページコンポーネントがサーバにデータを再要求するので元のレビューデータが表示されます。

DB削除命令

ゴミ箱ボタンが押されたら、DBに削除命令が走るように、追記していきます。
Prisma操作になるので、サーバー側で操作が必要になります。サーバーアクションを定義し、クライアントコンポーネントへimportすることでサーバー操作を扱うことができます。
サーバーアクションをまとめて定義する場所を作るので、lib/の下にactions.tsを作成します。

lib/actions.ts
'use server'

import { prisma } from "./prisma";

export const handleDeleteReview = async (reviewId: number) => {
  try{
      const deleteReview = await prisma.review.delete({
          where:{
              id: reviewId,
          }
      })
  }catch (err) {
      console.error(`レビューの削除に失敗しました。レビューID: ${reviewId}, エラーメッセージ: `, err);
      throw err
  }
}
components/ReviewCardContainer/ReviewCardContainer.tsx
'use client'

import { ReviewWithProductId } from "@/app/types";
import styles from "./CardContainer.module.css";
import ReviewCard from "@/app/components/ReviewCard/ReviewCard";
import { useState } from "react";
+ import { handleDeleteReview } from "@/lib/actions";

type Props = {
  title: string;
  reviews: ReviewWithProductId[];
}

export default function ReviewCardContainer(props: Props) {

  const [reviews, setReviews] = useState(props.reviews);

  const handleDelete = async (review : ReviewWithProductId) => {
    const answer = confirm(`レビュー記事「${review.title}」を削除しますか?`);
    if(answer){
+      try{
+        //delete DB data
+        handleDeleteReview(review.id)

        //update reviews
        const newReviews = reviews.filter((e) =>  e.id !== review.id )
        setReviews(
          newReviews
        )
+      }catch(err){
+          console.log("レビューの削除に失敗しました。ページを更新後、もう一度お試しください。")
+      }
    }
  }

  
  return ( 

    <div>
      <hr />
      <div className={styles.titleContainer}>
        <h2>{props.title}</h2>
        <p>{reviews.length}</p>
      </div>
      {reviews.length == 0 
      ?  (<p className={styles.noPost}>投稿はありません。</p>) 
      :  reviews.map((review, index) =>(
          <ReviewCard key={index} review={review} onDeleteReview={(review) => handleDelete(review)} />
        ))}
    </div>
  )
}

親のサーバーコンポーネントdashboard/review/page.tsxで同じ関数を定義してPropsとして渡すこともできますが、3つあるReviewCardContainerそれぞれに同じPropsを渡さなければならず、記述量が増えて見にくくなるので今回はこの方法にしました。すべて共通で使う機能ですし。
クライアントコンポーネントでも、サーバ処理が記述できるのは感動しました。この機能に気づく前は、せっせと内部API作ってfetchとか使ってました。記述量も処理速度も、サーバーアクションのほうが優れているように思います。

エラーハンドリングに関しては一旦適当な文言で仮置きです。こちらも奥が深そうなので、もう少し開発を進めてからの課題とします。

次回

Cloudinaryを利用して、画像の保存と表示などなど、やっていきます。
Youtubeの出し分けなども必要です。

Supabaseなどのほうが使い慣れてらっしゃる方は、そちらを利用していただいても全然問題ないです。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?