2

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.

Prateek Gupta
  • 2,422
  • 2
  • 16
  • 30
ale917k
  • 1,494
  • 7
  • 18
  • 37

1 Answers1

1

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])
subashMahapatra
  • 6,339
  • 1
  • 20
  • 26
  • Legend, that worked. But what about the empty array for dependencies? is that not a bad pattern? Since I'm using eslint, I get this warning on exhaustivedeps: React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array react-hooks/exhaustive-deps – ale917k Jun 09 '20 at 19:10
  • This is not an anti-pattern in this case. Because how `auth.onAuthStateChanged` works `useEffect` has to be set up like this, there is no other way unless you want to refactor the component to be a class and use `componentDidMount` and `componentWillUnmount` lifecycle methods. – subashMahapatra Jun 09 '20 at 19:15
  • Wait I don't see why you need to use the `user` variable in the same `useEffect`, do you still have `console.log(user)` in the same `useEffect`. If yes that's why `eslint` is complaining. Move the `console.log(user)` to a new `useEffect`. – subashMahapatra Jun 09 '20 at 19:19
  • If you want to log the `user` variable when the `user` state change you have to use another `useEffect` hook because the `useEffect` with the auth subscription in it needs an empty dependency array. See the EDIT section in updated answer. – subashMahapatra Jun 09 '20 at 19:22
  • Ahh yeah that's a good point. I found a solution for that by just commenting the warning (// eslint-disable-line react-hooks/exhaustive-deps), but your way of bringing the user on a different useEffect surely work better! – ale917k Jun 10 '20 at 16:33