はじめに
ReactでKPOPカードシェアアプリを開発しているともぼーです!
アプリの機能の1つとして、XやInstagramなどで見かけるいいね機能を実装してみようと思い今回は作成における過程をこの記事でまとめたいと思います。
事前環境
- Vite
- React
- typescript
- tailwindcss
- shadcn
それぞれの環境構築は今回は割愛します。
作成した機能
いいねボタンを押下すると、ハート色といいねカウント数が変わります。
いいね機能について
- いいね出来る数は1カードにつき「1いいね」※ユーザごとに
- いいねボタン押下で「いいね押下エフェクト」、いいねボタン再押下で「いいね押下前エフェクト」
■作成の流れ
-
supabase
でユーザ管理用、カード用、いいね用のテーブルを作成する - いいねボタンを作成する
- 「いいね機能について」の仕様を作成する
1.supabase
でカード用、いいね用のテーブルを作成する
Users
テーブル(ユーザ管理用テーブル)
ユーザごとのアカウントを管理します。
ログイン画面で入力したユーザIDをこのテーブルで管理することで、一意なユーザIDを保持します。
カラム名 | データ型 | 説明 |
---|---|---|
id |
int8 |
一意なID (主キー) |
user_id |
int8 |
ユーザーのID (重複なし) |
created_at |
timestamp with time zone |
デフォルト値: now()
|
UserCards
テーブル(カード用テーブル)
カードごとの情報を管理します。
user_id
と id
は likes
テーブルから外部キーとして参照されます。
カラム名 | データ型 | 説明 |
---|---|---|
id |
int8 |
カードの一意なID (主キー) |
user_id |
text |
ユーザID |
card_name |
text |
タイトル |
created_at |
timestamp |
デフォルト値: now()
|
group |
text |
KPOPグループ名 |
member |
text |
グループのメンバー名 |
image |
text |
画像の保存先パス |
movie |
text |
動画リンク |
music |
text |
音楽動画リンク |
like_memo |
text |
推しの好きなところメモ |
likes
テーブル(いいね用テーブル)
カードごとのいいね数を管理します。
同じカードIDの is_liked
が TRUE
の数だけいいね数を表示します。
カラム名 | データ型 | 説明 |
---|---|---|
user_id |
text |
いいねしたユーザーのID (UserCards テーブルの user_id を参照) ※主キー |
card_id |
int8 |
いいねされたカードのID (UserCards テーブルの id を参照) ※主キー |
is_liked |
boolean |
いいねフラグ(TRUE:いいね後、FALSE:いいね前) |
created_at |
timestamp with time zone |
いいねした日時 (デフォルトで now() ) |
用意するsupabaseのAPI
supabaseのAPIの使用方法は下記を参照してください。ここでは割愛して、用意する関数を列挙します。
■関数の列挙
-
likes
テーブルに新規のテーブル行を追加- insertLike(card_id: string, user_id: string)
-
likes
テーブルから対象カードID & ユーザIDのis_liked
を更新- updateLike(card_id: string, user_id: string, is_liked: boolean)
-
likes
テーブルから対象カードID & ユーザIDのis_liked
を取得- getIsLiked(card_id: string, user_id: string)
-
likes
テーブルの同じcard_id
のis_liked
をすべて取得- getLikedSameCardId(card_id: string)
export const insertLike = async (card_id: string, user_id: string) => {
const { data, error } = await supabase
.from("likes")
.insert([{ card_id, user_id }])
.select();
if (error) {
console.error("Error insert likes DB", error);
throw new Error("Failed to insert likes DB");
}
console.log("likes added successfully:", data);
};
export const getIsLiked = async (card_id: string, user_id: string) => {
const { data, error } = await supabase
.from("likes")
.select("is_liked")
.eq("card_id", card_id)
.eq("user_id", user_id);
if (error) {
console.log("not exist likes DB", error);
}
console.log("likes get successfully:", data);
return data;
};
export const updateLike = async (
card_id: string,
user_id: string,
is_liked: boolean,
) => {
const { data, error } = await supabase
.from("likes")
.update({ is_liked })
.eq("card_id", card_id)
.eq("user_id", user_id)
.select();
if (error) {
console.error("Error update likes DB", error);
throw new Error("Failed to update likes DB");
}
console.log("likes update successfully:", data);
};
export const getLikedSameCardId = async (card_id: string) => {
const { data, error } = await supabase
.from("likes")
.select("*")
.eq("card_id", card_id)
.eq("is_liked", true);
if (error) {
console.error("Error get likes same card_id row", error);
throw new Error("Failed to likes same card_id row");
}
console.log("likes get likes same card_id row successfully:", data);
return data;
};
2.いいねボタンの作成
-
isLiked
が TRUEの時はハートアイコンボタンをピンク色にする -
isLiked
が FALSEの時はハートアイコンボタンを白色にする
import { FC } from "react";
import { Button } from "../ui/button";
import { BsFillHeartFill } from "react-icons/bs";
type Props = {
likedCount: number;
isLiked: boolean;
handleClick: React.MouseEventHandler<HTMLButtonElement> | undefined;
};
export const HartButton: FC<Props> = (props) => {
const { likedCount, isLiked, handleClick, ...rest } = props;
return (
<Button
variant="outline"
size="sm"
className="dark:text-neutral-4 border-pink-300 bg-transparent pr-6 pl-6 text-pink-500 hover:bg-pink-50 dark:border-neutral-700 dark:focus:ring-neutral-600"
onClick={handleClick}
{...rest}
>
<BsFillHeartFill
className={`${isLiked ? "text-pink-500" : "stroke-gray-900 stroke-[0.2px] text-white"}`}
/>
<span className="text-pink-500">いいね! {likedCount}</span>
</Button>
);
};
2.「いいね機能について」の仕様を作成する
...
type Props = {
card: UserCardClass;
};
export const UserCard: FC<Props> = (props) => {
const { card } = props;
// ローディング中を待つためのステート
const [loading, setLoading] = useState<boolean>(true);
// UserCardsテーブルから取得したカード情報を保持するステート
const [userCard, setUserCard] = useState<UserCardClass>();
// いいねフラグのステート
const [isLiked, setIsLiked] = useState(false);
// likesテーブルにいいねしたユーザID & カードIDが存在しているかフラグ
// これが存在していない(false)の場合、likesテーブルを更新ではなく新規で行追加する
const [isExistLikeFlag, setIsExistLikeFlag] = useState(false);
// いいねされた数を保持するステート
const [likedCount, setLikedCount] = useState(0);
// UserCardsテーブルの画像パスを保持するステート
const [memberImage, setMemberImage] = useState<string | ArrayBuffer | null>(
null,
);
// いいね数を更新する関数
const updateLikedCount = useCallback(async () => {
// いいねボタンを押下したカードIDのすべてのいいね数を計算
const likedJson = await getLikedSameCardId(card.id);
const newLikedCount = likedJson.length;
setLikedCount(newLikedCount);
console.log(`カードID:${card.id}のいいねされた数`, newLikedCount);
}, [card]);
useEffect(() => {
// いいねフラグの更新
const updateIsLiked = async () => {
const gotIsLiked = await getIsLiked(card.id, card.user_id);
if (gotIsLiked && gotIsLiked.length != 0) {
// likesテーブルにすでにデータが存在していたら is_likedフラグを更新する
setIsLiked(gotIsLiked[0].is_liked);
setIsExistLikeFlag(true);
}
};
// カード情報の更新
setUserCard(card);
// いいねフラグの更新
updateIsLiked();
// いいね数の更新
updateLikedCount();
// 画像の表示
// ...
setLoading(false);
}, [card, updateLikedCount]);
// いいねボタンを押下した時のいいねフラグの更新 or 追加、カードのいいね数の更新
const onClickGoodButton = useCallback(async () => {
const newIsLiked = !isLiked;
try {
// 1ユーザにつき、1カードに対していいね出来る数は1回のみ
// ユーザIDがカードに対していいねした回数を管理する
setIsLiked(newIsLiked);
if (isExistLikeFlag) {
await updateLike(card.id, card.user_id, newIsLiked);
} else {
/* 初回のいいねはTRUE固定 */
await insertLike(card.id, card.user_id);
}
updateLikedCount();
} catch {
if (newIsLiked) {
alert("いいねの追加中にエラーが発生しました。");
} else {
alert("いいねの解除中にエラーが発生しました。");
}
}
}, [card, isLiked, isExistLikeFlag, updateLikedCount]);
if (loading) {
console.log("Card is loading...");
return <div>ローディング中...</div>;
}
return (
<Card
className="overflow-hidden border-pink-100 bg-white/90 backdrop-blur-sm transition-all duration-300 hover:scale-105 hover:shadow-xl"
data-testid={`card-${userCard!.id}`}
>
<CardHeader>
...
</CardHeader>
<CardContent className="p-4">
...
{/* いいね!99 のようないいねボタン */}
<div className="mt-3 flex justify-center gap-2">
<HartButton
likedCount={likedCount}
isLiked={isLiked}
handleClick={onClickGoodButton}
/>
...
</div>
</CardContent>
</Card>
);
};
これでいいねボタンを押下すると、動画のようにいいね表示が切り替わり、いいね数がカウントアップされるようになりました!!
おわりに
今回、SNSでよく見るいいね機能を実装してみましたが、
テーブルの作りをしっかりと構築できれば、いいねの表示切り替えの処理実装に関してはシンプルでした。
難しそう!と思う機能でも、実装してみれば意外と出来ますね!