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

React useState与Context问题:取消订阅函数始终为undefined

解决Next.js+Firebase中Context传递取消订阅函数为undefined的问题

问题核心

你在开发Next.js+Firebase应用时,尝试将Firestore监听器的取消订阅函数存入React Context,但消费组件中始终获取到undefined。根源是循环依赖:自定义钩子useFirebaseAuth内部调用useAuth获取Context的setter,但useAuth依赖于AuthUserProvider,而AuthUserProvider又在渲染时调用useFirebaseAuth,导致useFirebaseAuth初始化时拿到的是Context的默认空函数,调用setter根本不会更新Provider中的状态。

解决方案

重构Context和状态管理逻辑,将Firebase相关逻辑直接整合到AuthUserProvider内部,消除循环依赖,同时规范状态更新方式。

修改后的完整代码(AuthUserContext.tsx)

"use client";
import { UserDocument, UserReservation } from "@/types/user";
import { useContext, createContext, useState, useEffect } from "react";
import { onAuthStateChanged, User } from "firebase/auth";
import { auth, db } from "@/firebase/config";
import { doc, onSnapshot, collection, query, where } from "firebase/firestore";

// 初始化用户状态
const initAuthUser = {
  uid: "",
  email: "",
  displayName: "",
  emailVerified: false,
  phoneNumber: "",
  photoURL: "",
  userDocument: {} as UserDocument,
  userReservation: [] as UserReservation[],
};

// 扩展Firebase User类型,添加自定义字段
interface ExtendedUser extends Omit<User, "userDocument" | "userReservation"> {
  userDocument?: UserDocument;
  userReservation?: UserReservation[];
}

// 创建Context
const authUserContext = createContext({
  authUser: initAuthUser,
  authUserIsLoading: true,
  reloadAuthUserData: () => {},
  unsubscribeDocuments: () => {},
  unsubscribeUserReservations: () => {},
});

interface AuthUserProviderProps {
  children: React.ReactNode;
}

export function AuthUserProvider({ children }: AuthUserProviderProps) {
  const [authUser, setAuthUser] = useState<ExtendedUser | typeof initAuthUser>(initAuthUser);
  const [authUserIsLoading, setAuthUserIsLoading] = useState<boolean>(true);
  const [unsubscribeDocuments, setUnsubscribeDocuments] = useState<() => void>(() => {});
  const [unsubscribeUserReservations, setUnsubscribeUserReservations] = useState<() => void>(() => {});

  // 重新加载用户数据
  const reloadAuthUserData = () => {
    if (auth.currentUser) {
      auth.currentUser.reload().then(() => {
        authStateChanged(auth.currentUser);
      });
    }
  };

  // 格式化Firebase用户数据
  const formatAuthUser = (user: User) => ({
    uid: user.uid,
    email: user.email,
    displayName: user.displayName,
    emailVerified: user.emailVerified,
    phoneNumber: user.phoneNumber,
    photoURL: user.photoURL,
  });

  // 获取用户预约数据并设置监听器
  const getUserReservations = async (user: ExtendedUser) => {
    const reservationsRef = collection(db, "reservations");
    const q = query(reservationsRef, where("uidUser", "==", user.uid));

    const unsubscribeResa = onSnapshot(q, (querySnapshot) => {
      const reservations = querySnapshot.empty 
        ? [] 
        : querySnapshot.docs.map(doc => doc.data() as UserReservation);
      
      setAuthUser(prev => ({ ...prev, userReservation: reservations }));
      setAuthUserIsLoading(false);
    });
    setUnsubscribeUserReservations(unsubscribeResa);
  };

  // 获取用户文档并设置监听器
  const getUserDocument = async (user: ExtendedUser) => {
    if (auth.currentUser) {
      const documentRef = doc(db, "users", auth.currentUser.uid);
      
      const unsubscribeDoc = onSnapshot(documentRef, (doc) => {
        const userDoc = doc.exists() ? doc.data() as UserDocument : {} as UserDocument;
        setAuthUser(prev => ({ ...prev, userDocument: userDoc }));
      });
      setUnsubscribeDocuments(unsubscribeDoc);
    }
  };

  // 处理用户认证状态变化
  const authStateChanged = async (authState: User | null) => {
    if (!authState) {
      // 用户注销时重置状态并取消所有监听器
      setAuthUser(initAuthUser);
      setAuthUserIsLoading(false);
      unsubscribeDocuments();
      unsubscribeUserReservations();
      return;
    }

    setAuthUserIsLoading(true);
    const formattedUser = formatAuthUser(authState);
    setAuthUser(formattedUser);
    
    await getUserDocument(formattedUser);
    await getUserReservations(formattedUser);
  };

  // 监听用户认证状态
  useEffect(() => {
    const unsubscribeAuth = onAuthStateChanged(auth, authStateChanged);
    // 组件卸载时取消所有监听器
    return () => {
      unsubscribeAuth();
      unsubscribeDocuments();
      unsubscribeUserReservations();
    };
  }, []);

  return (
    <authUserContext.Provider
      value={{
        authUser: authUser as typeof initAuthUser,
        authUserIsLoading,
        reloadAuthUserData,
        unsubscribeDocuments,
        unsubscribeUserReservations,
      }}
    >
      {children}
    </authUserContext.Provider>
  );
}

export const useAuth = () => useContext(authUserContext);

关键修改点

  1. 消除循环依赖:将useFirebaseAuth的逻辑直接整合到AuthUserProvider内部,不再在钩子中调用useAuth获取setter
  2. 规范状态更新:避免直接修改传入的用户对象(原代码中dataUserWithReservation直接修改参数),改用函数式更新setAuthUser(prev => ({...prev, ...newData}))
  3. 自动清理监听器:在用户注销、组件卸载时自动调用取消订阅函数,防止内存泄漏
  4. 简化Context结构:移除不必要的setter函数,直接在Provider内部管理取消订阅函数的状态

验证步骤

  1. 在UserAccountNavigation组件中通过useAuth获取取消订阅函数:
import { useAuth } from "@/contexts/AuthUserContext";

const UserAccountNavigation = () => {
  const { unsubscribeDocuments, unsubscribeUserReservations } = useAuth();

  const handleLogOutUser = async () => {
    console.log("unsubscribeDocuments", unsubscribeDocuments);
    console.log("unsubscribeUserReservations", unsubscribeUserReservations);
    
    // 先取消监听器再注销
    unsubscribeDocuments();
    unsubscribeUserReservations();
    
    // 执行注销逻辑
    // const { error } = await firebaseLogOutUser();
    // if (error) {
    //   toast.error(error.message);
    //   return;
    // }
    // toast.success("A bientôt !");
  };

  // ...其他组件逻辑
};
  1. 触发注销操作,控制台应打印出具体的函数引用而非undefined
  2. 确认Firestore控制台中对应的监听器已停止

内容的提问来源于stack exchange,提问作者BushiKodo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 08:00:56