For some reason, when inside my function "login", my "currentUser" state always returns null. What I'm trying to do is setting a snapshot on the user document when the user logs in.
I'm also not sure if I can save the unsubscribe function in state like this but that's a problem I didn't have the chance to have yet
My context component looks like this:
import React, { useContext, useState, useEffect } from 'react'
import { auth, db } from '../init/firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState("")
const [loading, setLoading] = useState(true)
// User firebase doc init
const [userDoc, setUserDoc] = useState({
loaded: false,
data: [],
})
const [firebaseUnsubscriber, setFirebaseUnsubscriber] = useState(null);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password)
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password).then(() => {
// The problem here is that "currentUser" is always null
const firebaseUnsubscriber = db.collection("users").doc(currentUser.uid).onSnapshot((doc) => {
setUserDoc({
loaded: true,
data: doc.data(),
})
});
setFirebaseUnsubscriber(firebaseUnsubscriber);
})
}
function logout() {
return auth.signOut().then(() => {
firebaseUnsubscriber();
})
}
useEffect(() => {
const unsubscriber = auth.onAuthStateChanged((user) => {
setCurrentUser(user)
setLoading(false)
})
return unsubscriber
}, [])
const value = {
currentUser,
userDoc,
login,
signup,
logout,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
CodePudding user response:
This is a matter of timing. When you call setCurrentUser
or any other setter returned from a useState()
call, you don't change the current value of the variable - you instead set the next value of the variable - which won't be used until React decides a rerender is needed.
Instead of making use of currentUser
from your state in the then
handler of signInWithEmailAndPassword()
, you should instead be using the UserCredential
object that is contained in the Promise.
function login(email, password) {
return auth
.signInWithEmailAndPassword(email, password)
.then((userCredential) => {
const firebaseUnsubscriber = db.collection("users")
.doc(userCredential.user.uid)
.onSnapshot((doc) => {
setUserDoc({
loaded: true,
data: doc.data(),
})
});
setFirebaseUnsubscriber(firebaseUnsubscriber);
});
}
Now while the above code will function to fix your issue, there are a number of problems that remain:
setFirebaseUnsubscriber
is storing an unsubscriber function in the state, which would cause a rerender which would mean that the unsubscriber should be invoked, which causes a new unsubscriber to be stored, which would mean that the unsubscriber should be invoked, which causes ... (the user data portion should be in its ownuseEffect
)- Errors with retrieving the user's data are silently ignored
- A newly created user won't have any user data to be loaded (and
data
will be set toundefined
instead of[]
like in the initial state) - A user who signed out doesn't have their data cleared
- Your
currentUser
state variable has a type ofUser | "" | null
. Either userUser | null
(recommended when usingloading
) orUser | null | undefined
. - The loading state of the current user isn't exposed to the context consumers
- The current code makes use of the should-be-considered-deprecated namespaced v8 Web SDK - if writing new code, you should be using the modular v9 Web SDK.
For a somewhat complex, but robust, way of fixing these issues, check out this answer and its version of FirebaseAuthUserContext
. At the time of writing, it uses the v8 SDK but I will update it to v9 in the future.