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

Next.js使用Firestore onSnapshot+useEffect报空值读取错误

问题根因

你的报错和异常行为来自5个明确的逻辑疏漏,和状态初始值无关:

  • Firestore 快照回调未做存在性校验:onSnapshot 触发时如果目标文档不存在、权限校验失败、路径错误,doc.data() 会直接返回null,你未做校验就把null写入poolData状态,后续读取属性时必然触发空指针错误。
  • React 闭包陈旧值问题:useEffect 传入空依赖数组[],导致内部注册的定时器回调永远捕获组件首次渲染时的poolData初始值,哪怕后续实时监听器更新了poolData,定时器里拿到的还是旧值,时序错乱时就会读到null触发报错。
  • 资源泄漏未清理:你注册了两个Firebase实时监听器,但effect的清理函数只清除了定时器,没有调用监听器返回的unsub方法,组件卸载后监听器仍在后台运行,会持续触发无效状态更新,甚至引发内存泄漏和异常。
  • 动态值时序错误:Next.js 首次hydration阶段router.query.slug初始值为undefined,Firebase Auth初始化完成前auth.currentUser也可能为null,你直接用这两个未就绪的值拼接文档路径建监听,实际监听的是不存在的无效路径,必然返回空快照。
  • 无效冗余逻辑:代码中调用的getFirstPoolSnap()未做任何定义,属于无效代码,不会解决任何问题。
修复方案

按以下逻辑调整代码即可解决问题:

  1. 等slug和auth.currentUser都初始化完成后,再注册Firestore监听器,不要在动态值为null/undefined时发起无效监听。
  2. 所有onSnapshot回调内先判断文档是否存在,再读取data()更新状态。
  3. 用useRef存储最新的poolData值,保证定时器回调永远能读到最新的状态值,避开闭包陈旧值问题。
  4. 清理函数内必须调用所有Firebase监听器的取消方法,避免资源泄漏。
  5. 定时器逻辑内增加空值守卫,poolData未就绪时直接跳过计算,不要执行后续属性读取逻辑。

修正后的完整代码:

import { useRouter } from 'next/router';
import { useState, useEffect, useRef } from 'react';
import { doc, onSnapshot } from 'firebase/firestore';
import { auth, db } from '@/your-firebase-config-path'; // 替换为实际的Firebase配置导入路径

export default function PoolPage() {
  const router = useRouter();
  const slug = router.query.slug;
  const [timerDisplay, setTimerDisplay] = useState(0);
  const [finalTime, setFinalTime] = useState(0);
  const [tokenState, setTokenState] = useState(0);
  const [open, setOpen] = useState(true);
  const [currentlyWinning, setCurrentlyWinning] = useState("Nobody");
  const [poolData, setPoolData] = useState(null);
  // 用ref存储最新poolData,供定时器回调读取,避开闭包陈旧值问题
  const poolDataRef = useRef(null);

  // 同步ref值,保证ref中永远是最新状态
  useEffect(() => {
    poolDataRef.current = poolData;
  }, [poolData]);

  useEffect(() => {
    // 路由参数、用户信息未就绪时直接退出,不建立无效监听
    if (!slug || !auth.currentUser?.uid) return;
    const profile = `/profile/${auth.currentUser.uid}`;

    // 监听pool文档变更
    const unsubPoolListener = onSnapshot(doc(db, "pools", slug), (doc) => {
      // 先校验文档存在性,避免读取null的data
      if (!doc.exists()) {
        setPoolData(null);
        return;
      }
      const data = doc.data();
      setPoolData(data);
    });

    // 监听用户token变更
    const unsubTokenListener = onSnapshot(
      doc(db, "users", auth.currentUser.uid),
      (doc) => {
        if (!doc.exists()) return;
        setTokenState(doc.data().tokens ?? 0);
      }
    );

    // 时间计算逻辑
    const getPoolTime = () => {
      const latestPoolData = poolDataRef.current;
      // 数据未就绪直接跳过计算
      if (!latestPoolData) return;

      const localTime = Math.floor(Date.now() / 1000);
      const gbTimeLeft = latestPoolData.expTimestamp.toMillis() / 1000 - localTime;
      const finalTimeLeft = latestPoolData.finalTime.toMillis() / 1000 - localTime;
      
      setOpen(latestPoolData.open);
      setTimerDisplay(gbTimeLeft < 0 || finalTimeLeft < 0 ? 0 : Math.floor(gbTimeLeft));
      setFinalTime(finalTimeLeft < 0 || gbTimeLeft < 0 ? 0 : Math.floor(finalTimeLeft));
      setCurrentlyWinning(latestPoolData.lastSetterName ?? "Nobody");
    };

    const interval = setInterval(getPoolTime, 1000);
    // 组件挂载、数据就绪后立即执行一次计算,无需等待1秒首屏才显示内容
    getPoolTime();

    // 统一清理所有副作用
    return () => {
      clearInterval(interval);
      unsubPoolListener();
      unsubTokenListener();
    };
  }, [slug, auth.currentUser?.uid]); // 正确声明依赖项,值变化时自动重建监听

  return (
    // 原有组件JSX逻辑
    <div></div>
  )
}
额外优化建议
  • 时间戳计算逻辑可以进一步优化:在onSnapshot拿到最新poolData时直接计算基准剩余时间,单独的1s定时器只做时间值递减,不需要每次都重复读取Firestore时间戳做计算,性能更好。
  • 涉及Firebase Auth的逻辑建议搭配onAuthStateChanged监听用户登录状态,不要直接读取auth.currentUser的瞬时值,从根源上避免用户未登录时的空值问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 21:42:13