Search code examples
reactjsfirebasegoogle-cloud-firestorereact-hooksuse-effect

How can I successfully unsubscribe from user notfiications (firebase, react)


my users have a collection in their userData document called notifications. I want to listen to these in realtime so you can see notifications instantly (like any social media app, facebook instagram etc).

In my app.js, I currently have this useEffect:

  useEffect(() => {
    //this gets the user data
    const unsubscribeUserData = async () => {
          const userData = await getUserByUserId(currentUser.uid);
          setData(userData);
    };
    //this subscribes to their notifications
  {
    /* 
    //This the unsub method
    const unsubscribe = db
      .collection("users")
      .doc(currentUser.uid)
      .collection("notifications")
      .orderBy("date", "desc")
      .onSnapshot((snapshot) => {
        let tempNotifications = [];
        snapshot.forEach((notification) => {
          console.log("adding notification: ", notification);
          tempNotifications.push(notification.data());
        });
        setNotifications(tempNotifications);
      });
    */
    }
    
    //What do I do with useRef?
    const unsubscribe = useRef();
   
    if (currentUser) {
      //If there is an auth object stored in my auth context, get their data and subscribe to their notifications
      unsubscribeUserData();
      unsubscribe();
    } else {
      //If there isn't, unsub from the notifications and set data to null
      setData(null);
      unsubscribe();
    }
    return () => {
      //If they have closed the webpage, unubscribe
      unsubscribe();
    };
  }, [currentUser]);

Can anyone give me a hint as to how to structure this so it unsubscribes successfully when needed?

Thanks!

EDIT (I believe this works!)

  useEffect(() => {
    const getUserData = async () => {
      const userData = await getUserByUserId(currentUser.uid);
      setData(userData);
    };

    if (currentUser) {
      getUserData();

      return (unsubscribeRef.current = db
        .collection("users")
        .doc(currentUser.uid)
        .collection("notifications")
        .orderBy("date", "desc")
        .onSnapshot((snapshot) => {
          let tempNotifications = [];
          snapshot.forEach((notification) => {
            console.log("adding notification: ", notification);
            tempNotifications.push(notification.data());
          });
          setNotifications(tempNotifications);
        }));
    } else {
      setData(null);
    }
  }, [currentUser]);

I believe that calling the return at the actual snapshot listener is the same as called a cleanup return, however doing it here skips the not a function error


Solution

  • The main issue is that useEffect hook callbacks can't be async functions as these implicitly return a Promise which would, by design, be interpreted as an useEffect hook cleanup function. This won't work.

    After looking at your code for a bit it looks like getting and setting the data state is independent of the firebase subscription. You can minify just the asynchronous logic into an async function to get and set the user data and invoke that, and access the firebase collection as per normal.

    Use a React ref to hold on to the unsubscribe callback.

    I think the following implementation should get you very close to what you are looking for. (Disclaimer: I've not tested this code but I think it should work)

    const unsubscribeRef = useRef();
    
    useEffect(() => {
      const getUserData = async () => {
        const userData = await getUserByUserId(currentUser.uid);
        setData(userData);
      };
    
      if (currentUser) {
        getUserData();
    
        unsubscribeRef.current = db
          .collection("users")
          .doc(currentUser.uid)
          .collection("notifications")
          .orderBy("date", "desc")
          .onSnapshot((snapshot) => {
            let tempNotifications = [];
            snapshot.forEach((notification) => {
              console.log("adding notification: ", notification);
              tempNotifications.push(notification.data());
            });
            setNotifications(tempNotifications);
          });
      } else {
        setData(null);
      }
    
      return () => {
        // Unsubscribe any current subscriptions before rerender/unmount
        // Note: uses Optional Chaining operator, if this can't work for you
        // use normal null-check:
        //   unsubscribeRef.current && unsubscribeRef.current()
        unsubscribeRef?.current();
      };
    }, [currentUser]);