ChatGPTを使用したアプリ開発記【フォロー・フォロワー機能の実装】

## 結論
ユーザー情報がFireStoreから取得できず、実装できませんでした。
Firebase error : failed to get document because the client is offline
<p>

![スクリーンショット 2025-05-11 13.19.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058366/4df49c0c-9533-4777-9643-c045bbf5b67d.png)

## Firestoreのデータ設計
```json
/users/{userId} {
  uid: "abc123",
  displayName: "たろう",
  followers: ["uid1", "uid2"],   // 自分をフォローしている人
  following: ["uid5", "uid7"]    // 自分がフォローしている人
}
```
## フォローボタン機能の実装
```followvutton.js
import React, { useEffect, useState } from 'react';
import { db } from './firebase';
import {
  collection,
  query,
  where,
  getDocs,
  addDoc,
  deleteDoc,
  doc,
} from 'firebase/firestore';

function FollowButton({ currentUserId, targetUserId }) {
  const [isFollowing, setIsFollowing] = useState(false);
  const [followDocId, setFollowDocId] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const checkFollow = async () => {
      const q = query(
        collection(db, 'followers'),
        where('follower_id', '==', currentUserId),
        where('followed_id', '==', targetUserId)
      );
      const snapshot = await getDocs(q);
      if (!snapshot.empty) {
        setIsFollowing(true);
        setFollowDocId(snapshot.docs[0].id);
      } else {
        setIsFollowing(false);
        setFollowDocId(null);
      }
      setLoading(false);
    };

    if (currentUserId && targetUserId && currentUserId !== targetUserId) {
      checkFollow();
    } else {
      setLoading(false); // 自分自身は対象外
    }
  }, [currentUserId, targetUserId]);

  const toggleFollow = async () => {
    setLoading(true);
    if (isFollowing && followDocId) {
      await deleteDoc(doc(db, 'followers', followDocId));
      setIsFollowing(false);
      setFollowDocId(null);
    } else {
      const docRef = await addDoc(collection(db, 'followers'), {
        follower_id: currentUserId,
        followed_id: targetUserId,
      });
      setIsFollowing(true);
      setFollowDocId(docRef.id);
    }
    setLoading(false);
  };

  if (loading || currentUserId === targetUserId) return null;

  return (
    <button
      onClick={toggleFollow}
      style={{
        backgroundColor: isFollowing ? '#ccc' : '#007bff',
        color: 'white',
        padding: '0.5rem 1rem',
        border: 'none',
        borderRadius: '8px',
        cursor: 'pointer',
        marginLeft: '1rem',
      }}
    >
      {isFollowing ? 'フォロー中' : 'フォロー'}
    </button>
  );
}

export default FollowButton;
```
1. import React, { useEffect, useState } from 'react';<p>
	•	解説: Reactと、useEffectおよびuseStateのフックをインポートしています。useStateは状態管理、useEffectは副作用を管理するために使用します。
<p>
2. import { db } from './firebase';<p>
	•	解説: FirebaseのFirestoreデータベースをdbとしてインポートします。このdbを使ってFirestoreにアクセスします。
<p>
3. import { collection, query, where, getDocs, addDoc, deleteDoc, doc } from 'firebase/firestore';<p>
	•	解説: Firestoreの関数やモジュールをインポートします。<p>
	•	collection: コレクションを参照するための関数。<p>
	•	query: クエリを作成するための関数。<p>
	•	where: 条件を指定するための関数。<p>
	•	getDocs: クエリに基づくドキュメントを取得するための関数。<p>
	•	addDoc: 新しいドキュメントをコレクションに追加するための関数。<p>
	•	deleteDoc: 指定したドキュメントを削除するための関数。<p>
	•	doc: 特定のドキュメントを参照するための関数。<p>
<p>
4. function FollowButton({ currentUserId, targetUserId }) {<p>
	•	解説: FollowButtonコンポーネントを定義します。currentUserId（現在ログイン中のユーザーのID）とtargetUserId（フォローされるユーザーのID）をプロパティとして受け取ります。
<p>
5. const [isFollowing, setIsFollowing] = useState(false);<p>
	•	解説: isFollowingという状態を作成し、ユーザーがターゲットユーザーをフォローしているかどうかを管理します。初期値はfalse（フォローしていない状態）です。
<p>
6. const [followDocId, setFollowDocId] = useState(null);<p>
	•	解説: followDocIdという状態を作成し、フォロー関係を示すFirestoreドキュメントのIDを保持します。このIDは、フォローを解除する際にドキュメントを特定するために使います。
<p>
7. const [loading, setLoading] = useState(true);<p>
	•	解説: loadingという状態を作成し、データがロード中かどうかを管理します。初期値はtrueにして、データのチェックが完了するまでローディング状態にします。
<p>
8. useEffect(() => { ... }, [currentUserId, targetUserId]);<p>
	•	解説: useEffectを使って副作用を管理します。この中の処理はcurrentUserIdまたはtargetUserIdが変更されるたびに実行されます。
	•	目的は、ターゲットユーザーが現在のユーザーに対してフォローされているかどうかを確認することです。
<p>
9. const checkFollow = async () => { ... };<p>
	•	解説: 非同期関数checkFollowを定義します。この関数は、Firestoreのfollowersコレクションをクエリして、現在のユーザーがターゲットユーザーをフォローしているかどうかを確認します。
<p>
10. const q = query(collection(db, 'followers'), where('follower_id', '==', currentUserId), where('followed_id', '==', targetUserId));<p>
	•	解説: followersコレクションから、follower_idがcurrentUserId、followed_idがtargetUserIdであるドキュメントを取得するクエリを作成します。
<p>
11. const snapshot = await getDocs(q);<p>
	•	解説: 上記のクエリを実行し、その結果をsnapshotとして取得します。
<p>
12. if (!snapshot.empty) { ... } else { ... }<p>
	•	解説: クエリ結果が空でない場合（すなわち、現在のユーザーがターゲットユーザーをフォローしている場合）は、isFollowingをtrueにし、followDocIdにドキュメントIDを設定します。空であれば、isFollowingをfalseにします。
<p>
13. setLoading(false);<p>
	•	解説: データの取得が終わったので、loadingをfalseに設定してローディング状態を終了します。
<p>
14. if (currentUserId && targetUserId && currentUserId !== targetUserId) { checkFollow(); } else { setLoading(false); }<p>
	•	解説: currentUserIdとtargetUserIdが有効かつ、両者が異なるユーザーである場合に、checkFollow関数を実行します。自分自身のフォローは不要なので、自己フォローの場合はすぐにloadingをfalseに設定します。
<p>
15. const toggleFollow = async () => { ... };<p>
	•	解説: toggleFollow関数を定義します。この関数はフォロー／フォロー解除のトグル処理を行います。
<p>
16. setLoading(true);<p>
	•	解説: フォロー／フォロー解除処理中にローディング状態にします。
<p>
17. if (isFollowing && followDocId) { await deleteDoc(doc(db, 'followers', followDocId)); setIsFollowing(false); setFollowDocId(null); } else { const docRef = await addDoc(collection(db, 'followers'), { follower_id: currentUserId, followed_id: targetUserId }); setIsFollowing(true); setFollowDocId(docRef.id); }<p>
	•	解説: isFollowingがtrueの場合はフォロー解除を行い、Firestoreから該当ドキュメントを削除します。isFollowingがfalseの場合はフォローを追加し、followersコレクションに新しいドキュメントを作成します。
<p>
18. setLoading(false);<p>
	•	解説: 処理が終了したらローディング状態を解除します。
<p>
19. if (loading || currentUserId === targetUserId) return null;<p>
	•	解説: loading状態がtrueの場合や、currentUserIdとtargetUserIdが同じ場合は、フォローボタンを表示しません。
<p>
20. return ( ... );<p>
	•	解説: 最後にフォローボタンを返します。ボタンの色は、isFollowingがtrueであれば灰色、falseであれば青色になります。また、ボタンにはフォロー中またはフォローというラベルが表示されます。

終わりに

このコンポーネントは、Firebase Firestoreを使ってユーザーのフォロー状態を管理するシンプルな機能を実装しています。toggleFollow関数でフォロー／フォロー解除を切り替え、その状態をUIに反映します。

## プロフィール画面の実装
```profile.js

  // FollowButtonというReactコンポーネントを定義しています。
  // currentUserId: 現在ログインしているユーザーのID。
  // targetUserId: フォロー対象のユーザーのID。
function FollowButton({ currentUserId, targetUserId }) {

  	// フォロー状態を保持するためのstate。
	// trueなら「フォロー中」、falseなら「未フォロー」。
  const [isFollowing, setIsFollowing] = useState(false);

	// データ読み込み中かどうかを示すstate。
	// trueの間はボタンを読み込み状態にします。
  const [loading, setLoading] = useState(true);

  // フォロー状態を取得
  // コンポーネントのマウント（初回表示）や、依存変数が変更されたときに実行される処理。
  useEffect(() => {

  // フォロー状態を確認する非同期関数を定義。
    const fetchFollowStatus = async () => {

  // サーバーに対して、現在のフォロー状態を問い合わせるAPIを呼び出します
      const response = await fetch(`/api/follow-status?follower_id=${currentUserId}&followed_id=${targetUserId}`);

      // レスポンスをJSONとしてパース。
      const data = await response.json();

      // APIの結果に応じてフォロー状態を更新。
      setIsFollowing(data.isFollowing);

      // ローディング状態を解除。
      setLoading(false);
    };

    // 両方のIDが揃っているときのみチェック処理を実行。
    if (currentUserId && targetUserId) {
      fetchFollowStatus();
    }

   // currentUserIdまたはtargetUserIdが変わったら再実行。
  }, [currentUserId, targetUserId]);

// ボタンがクリックされたときに実行する関数。
  const toggleFollow = async () => {

  // ボタンを一時的に読み込み状態に。
    setLoading(true);

   // すでにフォローしているかをチェック。
    if (isFollowing) {
   
      // アンフォローするためにAPIにDELETEリクエストを送る。
      await fetch(`/api/unfollow?follower_id=${currentUserId}&followed_id=${targetUserId}`, { method: 'DELETE' });
    
     // フォローするためにAPIにPOSTリクエストを送信。
	// JSON.stringifyでボディを文字列に変換。
    } else {
      // フォロー
      await fetch(`/api/follow`, {
        method: 'POST',
        body: JSON.stringify({ follower_id: currentUserId, followed_id: targetUserId }),
      });
    }

    // 状態をトグル（反転）して、画面表示を更新。
    setIsFollowing(!isFollowing);

    読み込み終了。
    setLoading(false);
  };


  //下記return以降の処理
  // 読み込み中は、押せないボタンを表示。
  if (loading) return <button disabled>読み込み中...</button>;

  // 状態に応じて「フォロー中」または「フォロー」のラベルを切り替えるボタンを表示。
  return (
    <button onClick={toggleFollow}>
      {isFollowing ? 'フォロー中' : 'フォロー'}
    </button>
  );

  
```