2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SNSでよく見るいいね機能をReact × TypeScriptで作ってみた話。

Last updated at Posted at 2025-08-03

はじめに

ReactでKPOPカードシェアアプリを開発しているともぼーです!
アプリの機能の1つとして、XやInstagramなどで見かけるいいね機能を実装してみようと思い今回は作成における過程をこの記事でまとめたいと思います。

事前環境

  • Vite
  • React
  • typescript
  • tailwindcss
  • shadcn

それぞれの環境構築は今回は割愛します。

作成した機能

いいねボタンを押下すると、ハート色といいねカウント数が変わります。

無題の動画 (2).gif

いいね機能について

  • いいね出来る数は1カードにつき「1いいね」※ユーザごとに
  • いいねボタン押下で「いいね押下エフェクト」、いいねボタン再押下で「いいね押下前エフェクト」

■作成の流れ

  1. supabaseユーザ管理用、カード用、いいね用のテーブルを作成する
  2. いいねボタンを作成する
  3. 「いいね機能について」の仕様を作成する

1.supabase でカード用、いいね用のテーブルを作成する

Users テーブル(ユーザ管理用テーブル)

ユーザごとのアカウントを管理します。
ログイン画面で入力したユーザIDをこのテーブルで管理することで、一意なユーザIDを保持します。

カラム名 データ型 説明
id int8 一意なID (主キー)
user_id int8 ユーザーのID (重複なし)
created_at timestamp with time zone デフォルト値: now()

UserCards テーブル(カード用テーブル)

カードごとの情報を管理します。
user_ididlikes テーブルから外部キーとして参照されます。

カラム名 データ型 説明
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_likedTRUE の数だけいいね数を表示します。

カラム名 データ型 説明
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_idis_liked をすべて取得
    • getLikedSameCardId(card_id: string)
supabaseFunctions.ts
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の時はハートアイコンボタンを白色にする
HardButton.tsx
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.「いいね機能について」の仕様を作成する

UserCard.tsx
...
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でよく見るいいね機能を実装してみましたが、
テーブルの作りをしっかりと構築できれば、いいねの表示切り替えの処理実装に関してはシンプルでした。

難しそう!と思う機能でも、実装してみれば意外と出来ますね!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?