Fetching User Data from user collection on login

Viewed 867

I am currently working on an application with firebase initialized on the client side. When a user logs in via firebase, I want to fetch the user's data from the firestore. I am currently doing so within the onAuthStateChanged listener and successfully fetching the user. I am wondering if this is the best way to go about fetching the user data. My code is as follows:

  const [currentUser, setCurrentUser] = useState(null)
  const [authState, setAuthState] = useState(false)

  useEffect(() => {
    console.log('state unknown')
    setAuthState(false)
    auth().onAuthStateChanged(user => {
      if (!user) {
        return
      }
      const sourceRef = firestore()
        .collection('users')
        .where('userId', '==', user.uid)
      sourceRef
        .get()
        .then(snapshot => {
          if (snapshot.empty) {
            console.log('user not found')
          } else {
            let data = {}
            snapshot.forEach(item => (data = item.data()))
            console.log(data)
            setCurrentUser(data)
            setAuthState(true)
          }
        })
        .catch(error => console.log(error.code))
    })
  }, [])

  return (
    <AppContext.Provider value={{ currentUser, authState }}>
      {children}
    </AppContext.Provider>
  )
}

my main concern is that the user's data will be fetched every time the application is refreshed. Any suggestions or best practices on the matter would be greatly appreciated

1 Answers

IMHO your approach using context API and auth observer is already good, with only minor issues:

  • you should unsubscribe your observers as mentioned by Awran5
  • you could observe user data instead of fetching it only once
  • you could separate your contexts for readability and performance
  • you could create your documents in collection users with the id of the owner, so you can directly access collection("users").doc(authUser.uid) instead of doing a query

Here is how I would modify your example, not sure if it is the best way, I am awaiting on others comments:

const AuthContext = React.createContext();

function AuthProvider({ children }) {
    const [authUser, setAuthUser] = React.useState(undefined);

    React.useEffect(() => {
        return firebase.auth().onAuthStateChanged(user => setAuthUser(user));
    }, []);

    return <AuthContext.Provider value={authUser}>{children}</AuthContext.Provider>;
}
const UserDataContext = React.createContext();

function UserDataProvider({ children }) {
    const [userData, setUserData] = React.useState(null);
    const authUser = React.useContext(AuthContext);

    React.useEffect(() => {
        if (!authUser) return;
        const query = firebase.firestore().collection("/users").where("userId", "==", authUser.uid);
        return query.onSnapshot(snapshot => setUserData(snapshot.empty ? null : snapshot.docs[0].data()));
    }, [authUser]);

    return <UserDataContext.Provider value={userData}>{children}</UserDataContext.Provider>;
}
export default function App() {
    return (
        <AuthProvider>
            <UserDataProvider>
                {/* ... other components */}
                <MyComponent />
            </UserDataProvider>
        </AuthProvider>
    );
}
function MyComponent() {
    const authUser = React.useContext(AuthContext);
    const userData = React.useContext(UserDataContext);
    if (authUser === undefined) return <div>loading user authentication...</div>;
    if (authUser === null) return <div>logged out</div>;
    return <div>User data: {JSON.stringify(userData)}</div>;
}

Your components will still be re-rendered on auth state change, or on user data change, but only as needed.

Related