baoxiang/StreamerConsole修复报告.md
2025-12-16 18:06:50 +08:00

5.6 KiB
Raw Blame History

🔧 StreamerConsole React Hooks 错误修复报告

问题描述

用户在访问主播控制台页面时遇到页面空白,浏览器控制台报错:

React has detected a change in the order of Hooks called by StreamerConsole
Rendered more hooks than during the previous render

根本原因分析

1. React Hooks规则违反

  • 错误类型: 在条件渲染后定义了新的Hook
  • 问题位置: StreamerConsole.tsx 第175-265行
  • 具体表现:
    • loading检查后定义activeChests
    • 在条件渲染后又定义了countdowns等状态Hook
    • 违反了"Hooks必须始终在组件顶层调用"的规则

2. 类型定义错误

  • 问题: 使用了NodeJS.Timeout类型
  • 影响: 浏览器环境中该类型不存在
  • 报错: Cannot find namespace 'NodeJS'

修复方案

1. 重构Hook声明顺序

修复前(错误):

const StreamerConsole = () => {
  // 基础状态Hook
  const [myChests, setMyChests] = useState<Chest[]>([]);
  const [loading, setLoading] = useState(true);
  // ... 其他Hook

  // 权限检查(条件渲染)
  useEffect(() => {
    if (!user) {
      navigate('/login');
      return;
    }
    loadMyChests();
  }, [user, navigate]);

  // 加载状态检查(条件渲染)
  if (loading) {
    return <Loading text="加载控制台中..." />;
  }

  // 计算活跃宝箱(条件渲染后)
  const activeChests = myChests.filter(c => c.status === 0 || c.status === 1);

  // ❌ 错误在这里定义新的Hook
  const [countdowns, setCountdowns] = useState<{[key: number]: number}>({});
  const countdownIntervalsRef = useRef<{[key: number]: NodeJS.Timeout}>({});

修复后(正确):

const StreamerConsole = () => {
  // ✅ 所有Hook必须在组件顶部定义
  const { user } = useAuth();
  const navigate = useNavigate();

  // 基础状态Hook
  const [myChests, setMyChests] = useState<Chest[]>([]);
  const [loading, setLoading] = useState(true);
  const [showCreateForm, setShowCreateForm] = useState(false);
  // ... 其他状态Hook

  // ✅ 倒计时相关Hook也在顶部定义
  const [countdowns, setCountdowns] = useState<{[key: number]: number}>({});
  const countdownIntervalsRef = useRef<{[key: number]: number}>({});

  // 权限检查不影响Hook调用
  useEffect(() => {
    if (!user) {
      navigate('/login');
      return;
    }
    loadMyChests();
  }, [user, navigate]);

  // 加载状态检查不影响Hook调用
  if (loading) {
    return <Loading text="加载控制台中..." />;
  }

  // 计算逻辑不在Hook中
  const activeChests = myChests.filter(c => c.status === 0 || c.status === 1);

2. 修复类型定义

修复前:

// 前端环境中NodeJS.Timeout不存在
const intervalRef = useRef<NodeJS.Timeout | null>(null);

修复后:

// 浏览器环境使用number类型
const intervalRef = useRef<number | null>(null);

3. 清理未使用代码

删除了未使用的stopCountdown函数避免TypeScript警告。

修复结果

验证测试

  1. TypeScript编译: 通过

    npm run build
    # 之前的Hook错误已消失
    
  2. 开发服务器启动: 成功

    VITE v5.4.21 ready in 340 ms
    
    ➜  Local:   http://localhost:3001/
    
  3. 页面渲染: 正常

    • 不再出现空白页面
    • 倒计时功能正常工作
    • 控制台无错误

技术要点

React Hooks规则

  1. 只在最顶层调用Hook

    • 不要在循环、条件或嵌套函数中调用
    • 只在React函数中调用
  2. Hook顺序必须一致

    • 每次渲染时Hook的调用顺序必须相同
    • 条件渲染后不能定义新Hook
  3. 类型定义注意事项

    • 前端环境浏览器使用DOM API类型
    • NodeJS.Timeout是Node.js类型不适用于浏览器
    • 浏览器中使用number类型表示定时器ID

最佳实践

1. Hook组织结构

const Component = () => {
  // 1. 基础HookuseState, useRef等
  const [state1, setState1] = useState();
  const [state2, setState2] = useState();
  const ref = useRef();

  // 2. 计算HookuseMemo, useCallback
  const computed = useMemo(() => ..., []);

  // 3. 副作用HookuseEffect
  useEffect(() => {
    // 副作用逻辑
  }, []);

  // 4. 条件返回不影响Hook调用
  if (loading) {
    return <Loading />;
  }

  // 5. 渲染逻辑
  return <div>{computed}</div>;
};

2. 类型安全

// ✅ 正确:浏览器环境
const timerRef = useRef<number | null>(null);

// ❌ 错误Node.js类型
const timerRef = useRef<NodeJS.Timeout | null>(null);

预防措施

  1. 代码审查清单

    • 所有Hook在组件顶部定义
    • 没有Hook在条件渲染后定义
    • 使用正确的类型定义
  2. IDE配置

    • 启用TypeScript严格模式
    • 配置ESLint规则检查Hook使用
  3. 测试覆盖

    • 单元测试验证组件渲染
    • 集成测试验证用户交互

总结

本次修复解决了React Hooks使用错误导致的页面崩溃问题。通过正确组织Hook声明顺序和修复类型定义确保了

  • 页面正常渲染,无空白页
  • 倒计时功能正常工作
  • 无TypeScript类型错误
  • 代码符合React最佳实践

修复文件:

  • frontend/src/pages/StreamerConsole.tsx - 修复Hook顺序
  • frontend/src/components/ChestCard.tsx - 修复类型定义
  • frontend/src/services/websocket.ts - 修复类型定义

修复时间: 2025-12-15 修复状态: 完成 测试状态: 通过