プライベートが忙しくなっていまい、更新頻度が空いてしまいましたね。
とりあえず書き溜めていた記事だけでも、投稿がんばります!
削除ボタンを作成します
前回の続きです。
削除ボタンの機能を実装します。
クライアントコンポーネントの特徴を利用する
ReviewCardContainer
というクライアントコンポーネントが、レビュー記事の配列を親コンポーネントから受け取ってそのまま表示している状態です。
この状態でDB上のレビューデータを削除したとしても、親から渡されるPropsはページが更新でもされてサーバに再要求されない限りは、更新されずに古いデータのままになってしまいます。
削除ボタンを押すたびに更新処理をかけるのはUXの質の低下に繋がりかねません。
そこで、クライアントコンポーネントでのみ利用できる、ReactフックuseState
の出番です。
state フック
state を使うと、ユーザの入力などの情報をコンポーネントに「記憶」させることができます。例えば、フォームコンポーネントは入力された文字を保持し、画像ギャラリのコンポーネントは選択された画像を保持できます。
これを使って、Propsで渡ってきたレビュー記事の配列をStateに記憶させ、削除ボタンが押されたときはDBへの削除命令とsetStateの2つの操作を同時に行うことにします。
State操作
各コンポーネントを以下の通り更新します。
'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>
)
}
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環境で削除ボタンを押すと、レビューが消えるのが確認できます。
しかし、DBには削除命令を送っていないので、DB上にはデータが残ったままです。ページを更新すれば、ページコンポーネントがサーバにデータを再要求するので元のレビューデータが表示されます。
DB削除命令
ゴミ箱ボタンが押されたら、DBに削除命令が走るように、追記していきます。
Prisma操作になるので、サーバー側で操作が必要になります。サーバーアクションを定義し、クライアントコンポーネントへimportすることでサーバー操作を扱うことができます。
サーバーアクションをまとめて定義する場所を作るので、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
}
}
'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などのほうが使い慣れてらっしゃる方は、そちらを利用していただいても全然問題ないです。