skip to Main Content

I’m implementing 1:1 chatting on my Next.js website.

I’m encountering an issue with updating and properly unsubscribing listeners for unread messages count in Firebase Realtime Database. Here’s the scenario:

I have a function getUnreadMessages that listens to changes in the database to calculate the total unread messages count for a user across multiple chats. The function sets up onValue listeners for each chat’s unread messages count and accumulates the count. However, when I use another function setUnreadMessages to update the count for a specific chat, the listeners established by onValue do not update accordingly. This leads to outdated counts being returned by getUnreadMessages.

This is my firebase database structure

{
  "chats": {
    "-NsiGVfPascYlr1ukPVP": {
      "lastMessage": "sdf",
      "members": {
        "65dd85133a58aa82bc050081": true,
        "65e71757ff707f0da0b9e8d8": true
      },
      "timestamp": 1710242413808,
      "unreadMessages": {
        "65dd85133a58aa82bc050081": 0,
        "65e71757ff707f0da0b9e8d8": 5
      }
    },
    "one": {
      "lastMessage": "안녕",
      "members": {
        "65dd85133a58aa82bc050081": true,
        "65df0955c7a84d629f8a54a5": true
      },
      "timestamp": 1710220731677,
      "unreadMessages": {
        "65dd85133a58aa82bc050081": 0
      }
    }
  },
  "users": {
    "65dd85133a58aa82bc050081": {
      "chats": {
        "-NsiGVfPascYlr1ukPVP": true,
        "one": true
      }
    },
    "65df0955c7a84d629f8a54a5": {
      "chats": {
        "one": true
      }
    },
    "65e71757ff707f0da0b9e8d8": {
      "chats": {
        "-NsiGVfPascYlr1ukPVP": true
      }
    }
  }
}
export async function getUnreadMessages({
  userId,
}: {
  userId: string;
}): Promise<number> {
  let totalUnreadMessages = 0;

  const userChatsRef = ref(realtimeDB, `users/${userId}/chats`);
  const snapshot = await get(userChatsRef);
  const chatIds = snapshot.val() || {};

  const promises: Promise<void>[] = [];

  for (const chatId in chatIds) {
    const userChatRefRef = ref(
      realtimeDB,
      `chats/${chatId}/unreadMessages/${userId}`
    );

    const promise = new Promise<void>((resolve, reject) => {
      onValue(
        userChatRefRef,
        (snapshot: DataSnapshot) => {
          const unreadMessagesCount = snapshot.exists() ? snapshot.val() : 0;
          totalUnreadMessages += unreadMessagesCount;
          resolve();
        },
        (error) => {
          console.error(`Error listening to chat ${chatId}:`, error);
          reject(error);
        }
      );
    });

    promises.push(promise);
  }

  await Promise.all(promises);
  return totalUnreadMessages;
}
export async function setUnreadMessages({
  chatId,
  userId,
  isReset,
}: {
  chatId: string;
  userId: string;
  isReset?: boolean;
}): Promise<void> {
  if (!chatId || !userId) return;
  try {
    const unreadMessagesRef = ref(
      realtimeDB,
      `chats/${chatId}/unreadMessages/${userId}`
    );
    let unreadMessagesCount = 0;

    if (!isReset) {
      const unreadMessagesSnapshot = await get(unreadMessagesRef);

      if (unreadMessagesSnapshot.exists()) {
        unreadMessagesCount = unreadMessagesSnapshot.val() || 0;
      }
    }

    await set(
      unreadMessagesRef,
      isReset ? unreadMessagesCount : ++unreadMessagesCount
    );
    console.log(
      `Unread messages count for user ${userId} in chat ${chatId} reset.`
    );
  } catch (error) {
    console.error("Error resetting user unread messages count:", error);
    throw error;
  }
}

and I use react-query to fetch the data.

const { data: fetchedUnreadMessagesCount } = useQuery({
    queryKey: ["unreadMessagesCount"],
    queryFn: () => getUnreadMessages({ userId: session?.user.id }),
    enabled: !!session?.user.id,
  });

My firebase version is v10.8.1.
Please tell me where I’m doing wrong? Thank you in advance!

2

Answers


  1. Chosen as BEST ANSWER

    I created a custom hook that listens the onValue

    type UseUnreadMessagesCountType = {
      userId: string;
    };
    
    export default function useUnreadMessagesCount({
      userId,
    }: UseUnreadMessagesCountType) {
      const [chats, setChats] = useState<Record<string, number> | null>({} || null);
      const { chatsId } = useMyChats();
    
      useEffect(() => {
        if (!chatsId || !userId) return;
        let unsubscribeFunctions: Unsubscribe[] = [];
    
        const cleanup = () => {
          unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
        };
    
        unsubscribeFunctions = Object.keys(chatsId).map((chatId) => {
          const userChatRefRef = ref(
            realtimeDB,
            `chats/${chatId}/unreadMessages/${userId}`
          );
    
          const unsubscribe = onValue(
            userChatRefRef,
            (snapshot) => {
              const unreadMessagesCount = snapshot.exists() ? snapshot.val() : 0;
              setChats((prev) => ({
                ...prev,
                [chatId]: unreadMessagesCount,
              }));
            },
            (error) => {
              console.error(`Error listening to chat ${chatId}:`, error);
            }
          );
    
          return unsubscribe;
        });
    
        return cleanup;
      }, [chatsId, userId]);
    
      return sumObjectValues(chats);
    }
    

  2. If you want to listen for realtime updates to a value, you:

    • Can’t return that value, as a return statement can only run once.
    • Can’t use promises to do so, as a Promise can only resolve once.

    If you want to listen for updates, that can happen multiple times – so you need to use a mechanism that allows that. This is in fact precisely why the onValue API takes a callback method, rather than returning a Promise. You’ll need to do something similar.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search