取消onSnapshot监听器时触发Firebase权限不足错误求助
排查Firestore会话管理Hook中取消订阅逻辑失效问题
问题根源
- 变量作用域冲突:全局声明的
unsubscribe与setAppSession内部的局部const unsubscribe重名,内部变量覆盖了全局变量,导致全局unsubscribe始终为null,组件卸载时的清理函数无法触发真正的Firestore监听器取消订阅操作。 - 异步操作时序问题:
setAppSession是异步函数,当外部触发登出时,组件可能已经开始卸载,此时异步操作尚未完成,unsubscribe还未被赋值,清理函数自然无法执行。 - 全局变量设计缺陷:全局变量在组件复用或多次调用Hook时会引发状态混乱,违背React Hook的封装性原则。
解决方案
1. 用useRef替代全局变量存储取消订阅函数
useRef的.current属性可以在异步操作中修改,且不会触发组件重渲染,完美适配保存取消订阅函数的需求,同时避免全局状态污染。
2. 优化订阅生命周期管理
在组件更新(如currentUser变化)时,先取消之前的旧订阅,再创建新订阅;组件卸载时确保执行取消订阅逻辑。
3. 修正Session生成的依赖逻辑
当用户身份变化时,重新生成会话ID,避免复用旧会话数据。
修改后的完整代码
useSessionHandler Hook
// React import { useContext, useEffect, useMemo, useState, useRef } from 'react'; // Firebase import { db } from '../../../firebase/firebase'; import { Timestamp, doc, onSnapshot, setDoc } from 'firebase/firestore'; // Redux import { useAppSelector } from '../../../redux-store/hooks'; import { useDispatch } from 'react-redux'; // Context import { authContext } from '../../../context/auth/AuthState'; // Utilities import { errorHandling } from '../../../utilities/errorHandling'; import { generateId } from '../../../utilities/generalHelperFunctions'; import { getDeviceTypeOS } from '../../../utilities/userAgentHelperFunctions'; const useSessionHandler = () => { const { SignOut } = useContext(authContext); const currentUser = useAppSelector((state) => state.auth.currentUser); const dispatch = useDispatch(); const [isSessionValid, setIsSessionValid] = useState<boolean>(false); // 使用useRef存储取消订阅函数,避免全局变量污染 const unsubscribeRef = useRef<(() => void) | null>(null); // 用户身份变化时重新生成会话 const sessionData = useMemo(() => { if (!currentUser?.uid) return null; return { loginId: generateId(), deviceType: getDeviceTypeOS(), online: true, timeStamp: Timestamp.now().toDate().toString() }; }, [currentUser?.uid]); useEffect(() => { // 组件更新时先清理旧订阅 if (unsubscribeRef.current) { unsubscribeRef.current(); unsubscribeRef.current = null; } if (currentUser?.authenticated && currentUser.uid && sessionData) { const setupSession = async () => { try { // 创建或覆盖Firestore中的会话记录 await setDoc(doc(db, 'data-users-sessions', currentUser.uid), sessionData); // 监听会话变化,将取消订阅函数存入ref unsubscribeRef.current = onSnapshot( doc(db, 'data-users-sessions', currentUser.uid), (docSnapshot) => { const firestoreSession = docSnapshot.data(); if (firestoreSession) { if (sessionData.loginId !== firestoreSession.loginId) { unsubscribeRef.current?.(); SignOut('forceSignOut'); } else { setIsSessionValid(true); } } }, (error) => { console.log('Firestore监听错误', error); errorHandling(error, '会话监听异常', 'useSessionHandler onSnapshot', dispatch); } ); } catch (error) { errorHandling(error, null, '会话初始化失败', dispatch); } }; setupSession(); } // 组件卸载时清理订阅 return () => { if (unsubscribeRef.current) { unsubscribeRef.current(); unsubscribeRef.current = null; } }; }, [currentUser, sessionData, SignOut, dispatch]); return isSessionValid; }; export default useSessionHandler;
AppPrivateRoute 组件
const AppPrivateRoute: React.FC<Props> = () => { const currentUser = useAppSelector((state) => state.auth.currentUser); const authenticating = useAppSelector((state) => state.auth.authenticating); const isSessionValid = useSessionHandler(); // Hook直接返回布尔值,简化使用 return authenticating ? ( <LoadingComponent action={'Authenticating'} /> ) : currentUser?.authenticated && isSessionValid ? ( <Outlet /> ) : ( <Navigate to='/' /> ); }; export default AppPrivateRoute;
关键修改说明
- 移除全局
unsubscribe变量,改用useRef存储取消订阅函数,解决作用域冲突问题。 - 在
useEffect开头增加旧订阅清理逻辑,防止用户状态变化时产生多个监听器。 - 调整
useMemo依赖为currentUser?.uid,确保用户切换时生成新的会话ID。 - 优化Hook返回值为直接布尔值,提升代码可读性和使用便捷性。
- 在监听器内部调用取消订阅时,使用可选链操作符
?.避免空值报错。
内容的提问来源于stack exchange,提问作者Richardson
相关产品推荐
相关产品推荐

