Stop infinite loop on useEffect using Firebase

Viewed 930

I was testing firebase with hooks for the first time and came across the well known infinite loop issue.

I know there are many others questions that may be close to this one, but I'm still not able to get around this issue on this situation.

Here the code on App.js:

import React, { useState, useEffect } from 'react';
import { auth, createUserProfileDocument } from './firebase/firebase.utils';

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    auth.onAuthStateChanged(async userAuth => {
      if (userAuth) {
        const userRef = await createUserProfileDocument(userAuth);

        userRef.onSnapshot(snapshot => {
          setUser({
            user: {
              id: snapshot.id,
              ...snapshot.data()
            }
          })
        });

        console.log(user);

      } else {
        setUser(null);
      }
    });
  }, [user]);

  return (
    <div></div>
  );
}

export default App;

Here the createUserProfileDocument function from firebase.utils.js:

export const createUserProfileDocument = async (userAuth, additionalData) => {
    if (!userAuth) return;

    const userRef = firestore.doc(`users/${userAuth.uid}`);

    const snapshot = await userRef.get();

    if (!snapshot.exists) {
        const { displayName, email } = userAuth;
        const createdAt = new Date();

        try {
            await userRef.set({
                displayName,
                email,
                createdAt,
                ...additionalData
            })
        } catch (err) {
            console.log('Error creating user', err.message);
        }
    }

    return userRef;
};

In this case, how would I check if the onAuthStateChanged has actually changed? I get the impression the auth.onAuthStateChanged function triggers each time, generating that infinite loop..

Here the resources I checked that may be helpful:

Thank you in advance.

1 Answers

The method onAuthStateChanged sets up a subscription by adding an observer for the user's sign-in state. You only need to subscribe once when the component mounts and call unsubscribe when the component unmounts to prevent the observer from running and causing memory leaks.

To answer why it is causing an infinite loop in your case, you have the user state as a dependency which will cause the observer to be reinitialized each time the state value user is updated and the observer will return a completely new userAuth object and setUser is called again which will update the user state which will reinitialize the observer and repeating the same loop over and over.

The solution,

setup the subscription only once when the component mounts by passing an empty array as dependency to useEffect

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // auth.onAuthStateChanged will return a firebase.Unsubrcibe function
    // which you can call to terminate the subscription
    const unsubscribe = auth.onAuthStateChanged(async userAuth => {
      if (userAuth) {
        const userRef = await createUserProfileDocument(userAuth);

        userRef.onSnapshot(snapshot => {
          setUser({
            user: {
              id: snapshot.id,
              ...snapshot.data()
            }
          })
        });

      } else {
        setUser(null);
      }
    });

   // return a clean up function that will call unsubscribe to -
   // terminate the subscription when component unmounts
   return () => { unsubscribe() }
  }, []); // important set an empty array as dependency

  return (
    <div></div>
  );
}

export default App;

EDIT- Do not try to log the user variable in the same useEffect hook because it needs an empty dependency array, instead use another useEffect hook to log the value of user passing the user as dependency. Example

useEffect(() => {
 console.log(user)
}, [user])
Related