I am trying to add an anonymous authentication functionality in Firebase. When user logs in anonymously, it is also creating an entry in Firebase Realtime Database.
The issue is that when user logs in anonymously, onAuthStateChanged
gets triggered immediately, which then triggers fetchCurrentUser
before handleAddUserToDb
finishes executing. Did I make a mistake?
If I add a delay on fetchCurrentUser
then the functions run in the correct order: handleAddUserToDb
then fetchCurrentUser
but adding a delay doesn't seem to be best practice...
const handleAnonymousLogin = async () => {
dispatch({ type: "loading" });
try {
const user = await signInAnonymously(auth);
await handleAddUserToDb(user.user.uid, "Guest", null, null);
navigate("/home");
} catch (error) {
console.error("Error signing in anonymously:", error);
dispatch({ type: "error" });
}
};
useEffect(() => {
dispatch({ type: "loading" });
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user) {
const id = user.uid;
await fetchCurrentUser(id);
} else {
dispatch({ type: "logged-out" });
}
});
return () => unsubscribe();
}, []);
const fetchCurrentUser = async (uid) => {
try {
const userRef = ref(db, `users/${uid}`);
onValue(userRef, (snapshot) => {
if (snapshot.exists()) {
dispatch({ type: "logged-in", payload: snapshot.val() });
} else {
alert("User not found.");
}
});
} catch (error) {
alert("Error fetching user: " + error.message);
}
};
Your onValue
is based on a callback which does not return a promise, so while the operation is asynchronous, you are not await
ing its result.
As far as I can see, there is no reason to use a realtime listener here though, and you're betting of getting a value once. The latter returns a promise, which means that you can use await
on it.
const fetchCurrentUser = async (uid) => {
try {
const userRef = ref(db, `users/${uid}`);
const snapshot = await get(userRef); // 👈
if (snapshot.exists()) {
dispatch({ type: "logged-in", payload: snapshot.val() });
} else {
alert("User not found.");
}
} catch (error) {
alert("Error fetching user: " + error.message);
}
};
Because we now use await
in fetchCurrentUser
, this function will also return a promise, which the makes await
that you already have when you call fetchCurrentUser
work.
Update: since apparently you want to use both a realtime listener and await the first result, you can return a custom promise from fetchCurrentUser
.
The code for that should be something like this:
const fetchCurrentUser = async (uid) => {
let isFirst = true;
return new Promise((resolve, reject) => {
try {
onValue(userRef, (snapshot) => {
// If this is the first data we get, resolve the promise so that the await is released
if (snapshot.exists()) {
if (isFirst) {
resolve(snapshot.val());
isFirst = false;
}
// For each time we get data, dispatch it to any listeners.
dispatch({ type: "logged-in", payload: snapshot.val() });
} else {
reject("User not found.");
}
});
} catch (error) {
reject("Error fetching user: " + error.message);
}
});
};