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);
关键修改点
- 消除循环依赖:将
useFirebaseAuth的逻辑直接整合到AuthUserProvider内部,不再在钩子中调用useAuth获取setter - 规范状态更新:避免直接修改传入的用户对象(原代码中
dataUserWithReservation直接修改参数),改用函数式更新setAuthUser(prev => ({...prev, ...newData})) - 自动清理监听器:在用户注销、组件卸载时自动调用取消订阅函数,防止内存泄漏
- 简化Context结构:移除不必要的setter函数,直接在Provider内部管理取消订阅函数的状态
验证步骤
- 在
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 !"); }; // ...其他组件逻辑 };
- 触发注销操作,控制台应打印出具体的函数引用而非
undefined - 确认Firestore控制台中对应的监听器已停止
内容的提问来源于stack exchange,提问作者BushiKodo
相关产品推荐
相关产品推荐

