221 lines
5.6 KiB
Markdown
221 lines
5.6 KiB
Markdown
# 🔧 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声明顺序
|
||
|
||
**修复前**(错误):
|
||
```typescript
|
||
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}>({});
|
||
```
|
||
|
||
**修复后**(正确):
|
||
```typescript
|
||
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. 修复类型定义
|
||
|
||
**修复前**:
|
||
```typescript
|
||
// 前端环境中NodeJS.Timeout不存在
|
||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
```
|
||
|
||
**修复后**:
|
||
```typescript
|
||
// 浏览器环境使用number类型
|
||
const intervalRef = useRef<number | null>(null);
|
||
```
|
||
|
||
### 3. 清理未使用代码
|
||
|
||
删除了未使用的`stopCountdown`函数,避免TypeScript警告。
|
||
|
||
## 修复结果
|
||
|
||
### ✅ 验证测试
|
||
|
||
1. **TypeScript编译**: ✅ 通过
|
||
```bash
|
||
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组织结构
|
||
```typescript
|
||
const Component = () => {
|
||
// 1. 基础Hook(useState, useRef等)
|
||
const [state1, setState1] = useState();
|
||
const [state2, setState2] = useState();
|
||
const ref = useRef();
|
||
|
||
// 2. 计算Hook(useMemo, useCallback)
|
||
const computed = useMemo(() => ..., []);
|
||
|
||
// 3. 副作用Hook(useEffect)
|
||
useEffect(() => {
|
||
// 副作用逻辑
|
||
}, []);
|
||
|
||
// 4. 条件返回(不影响Hook调用)
|
||
if (loading) {
|
||
return <Loading />;
|
||
}
|
||
|
||
// 5. 渲染逻辑
|
||
return <div>{computed}</div>;
|
||
};
|
||
```
|
||
|
||
### 2. 类型安全
|
||
```typescript
|
||
// ✅ 正确:浏览器环境
|
||
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
|
||
**修复状态**: ✅ 完成
|
||
**测试状态**: ✅ 通过
|