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

取消onSnapshot监听器时触发Firebase权限不足错误求助

排查Firestore会话管理Hook中取消订阅逻辑失效问题

问题根源

  1. 变量作用域冲突:全局声明的unsubscribe与setAppSession内部的局部const unsubscribe重名,内部变量覆盖了全局变量,导致全局unsubscribe始终为null,组件卸载时的清理函数无法触发真正的Firestore监听器取消订阅操作。
  2. 异步操作时序问题:setAppSession是异步函数,当外部触发登出时,组件可能已经开始卸载,此时异步操作尚未完成,unsubscribe还未被赋值,清理函数自然无法执行。
  3. 全局变量设计缺陷:全局变量在组件复用或多次调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 10:00:01