React+Firebase首次Google登录时Fetch返回undefined问题求助
新用户首次登录Firestore时获取数据报错的解决方案
问题场景
React项目使用Cloud Firestore作为数据库,通过Google账号登录,新用户首次登录时会自动在users集合创建文档。登录后通过Redux的createAsyncThunk获取用户数据并存入状态,但首次登录时会触发错误提示"An error occured while fetching user data",之后又能正常获取到数据。
相关代码如下:
登录逻辑代码
const signInWithGoogle = async () => { try { const res = await signInWithPopup(auth, googleProvider); const user = res.user; const q = query(collection(db, "users"), where("uid", "==", user.uid)); const docs = await getDocs(q); if(docs.docs.length === 0){ const firstName = user.displayName.split(' ')[0]; await addDoc(collection(db, "users"), { uid: user.uid, name: user.displayName, firstName: firstName, photoURL: user.photoURL, authProvider: "google", email: user.email, }) dispatch(getUser(user)) } } catch(err) { console.error(err); alert(err.message); } }
Auth状态监听代码
useEffect(() => { const unsubscribe = auth.onAuthStateChanged((user) => { setCurrentUser(user); setLoading(false); if(user){ dispatch(getUser(user)) } else { console.log("user logout") } }); return unsubscribe; }, []);
获取用户数据的Thunk代码
export const getUser = createAsyncThunk("profile/getUser", async (user) => { try { const userQuery = query( collection(db, "users"), where("uid", "==", user?.uid) ); const doc = await getDocs(userQuery); const data = doc.docs[0].data(); return data; } catch (err) { console.error(err); alert("An error occured while fetching user data"); } });
问题原因
核心问题是Firebase Auth的状态变化监听会先于Firestore文档创建完成:
- 新用户登录时,
signInWithPopup完成后Auth状态立即更新,触发onAuthStateChanged回调,此时getUser被dispatch执行 - 但Firestore的文档写入是异步操作,即使
addDoc用了await,Firestore的最终一致性可能导致短时间内查询不到刚创建的文档 - 此时
getUser里的doc.docs[0]为undefined,访问.data()直接抛出错误,触发catch里的提示框
解决方案
方案1:在getUser中增加空值判断与重试
修改getUser的Thunk逻辑,先检查查询结果是否为空,为空时短暂重试一次(适配Firestore的最终一致性),避免直接报错:
export const getUser = createAsyncThunk("profile/getUser", async (user) => { try { const userQuery = query( collection(db, "users"), where("uid", "==", user?.uid) ); let docSnapshot = await getDocs(userQuery); // 如果首次查询为空,短暂延迟后重试一次 if (docSnapshot.empty) { await new Promise(resolve => setTimeout(resolve, 150)); docSnapshot = await getDocs(userQuery); } // 重试后仍为空,返回null(后续在Redux切片中处理此情况) if (docSnapshot.empty) { return null; } return docSnapshot.docs[0].data(); } catch (err) { console.error(err); alert("An error occured while fetching user data"); } });
同时在Redux切片中处理null返回值,比如不更新状态或提示用户稍等。
方案2:创建文档后直接dispatch数据,避免重复查询
在signInWithGoogle中,创建完用户文档后直接把构造好的用户数据dispatch到Redux,省去再次查询Firestore的步骤:
const signInWithGoogle = async () => { try { const res = await signInWithPopup(auth, googleProvider); const user = res.user; const q = query(collection(db, "users"), where("uid", "==", user.uid)); const docs = await getDocs(q); if(docs.docs.length === 0){ const firstName = user.displayName.split(' ')[0]; // 预构造用户数据 const userData = { uid: user.uid, name: user.displayName, firstName: firstName, photoURL: user.photoURL, authProvider: "google", email: user.email, }; // 写入Firestore await addDoc(collection(db, "users"), userData); // 直接dispatch数据到Redux dispatch(setUser(userData)); // 需要在Redux切片中定义对应的同步action } else { // 老用户正常查询并dispatch dispatch(getUser(user)); } } catch(err) { console.error(err); alert(err.message); } }
并修改Auth状态监听逻辑,避免重复触发无效查询:
useEffect(() => { const unsubscribe = auth.onAuthStateChanged(async (user) => { setCurrentUser(user); setLoading(false); if(user){ const userQuery = query(collection(db, "users"), where("uid", "==", user.uid)); const docSnapshot = await getDocs(userQuery); // 仅当文档存在时才dispatch getUser if (!docSnapshot.empty) { dispatch(getUser(user)); } } else { console.log("user logout") } }); return unsubscribe; }, []);
方案3:使用实时监听替代单次查询
用Firestore的onSnapshot实时监听用户文档变化,文档创建完成后会自动触发状态更新,无需手动处理时序问题:
// 定义同步action用于更新用户状态 export const setUser = createAction("profile/setUser"); export const getUser = createAsyncThunk("profile/getUser", async (user, { dispatch }) => { try { const userQuery = query( collection(db, "users"), where("uid", "==", user?.uid) ); // 实时监听文档变化 const unsubscribe = onSnapshot(userQuery, (snapshot) => { if (!snapshot.empty) { const data = snapshot.docs[0].data(); dispatch(setUser(data)); } }); // 返回取消监听的函数,用于组件卸载时清理 return unsubscribe; } catch (err) { console.error(err); alert("An error occured while fetching user data"); } });
然后在组件中处理监听的清理:
useEffect(() => { let docUnsubscribe; const authUnsubscribe = auth.onAuthStateChanged((user) => { setCurrentUser(user); setLoading(false); if(user){ // 获取取消监听的函数 dispatch(getUser(user)).then(unsub => { docUnsubscribe = unsub; }); } else { console.log("user logout"); // 用户登出时取消监听 if(docUnsubscribe) docUnsubscribe(); } }); // 组件卸载时清理所有监听 return () => { authUnsubscribe(); if(docUnsubscribe) docUnsubscribe(); }; }, []);
内容的提问来源于stack exchange,提问作者Navih
相关产品推荐
相关产品推荐

