You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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文档创建完成:

  1. 新用户登录时,signInWithPopup完成后Auth状态立即更新,触发onAuthStateChanged回调,此时getUser被dispatch执行
  2. 但Firestore的文档写入是异步操作,即使addDoc用了await,Firestore的最终一致性可能导致短时间内查询不到刚创建的文档
  3. 此时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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 20:20:29