Files
vf_react/src/views/Community/components/StockDetailPanel/hooks/useEventStocks.js
zdl 3821db1b78 fix: 使用 shallowEqual 修复 useSelector 引用不稳定导致的无限循环
## 问题
仍然报错 "Maximum update depth exceeded",第一次修复不完整。

## 根本原因(第二轮诊断)
useSelector 返回的数组/对象引用不稳定:

**useEventStocks.js**
```javascript
const stocks = useSelector(state =>
  eventId ? (state.stock.eventStocksCache[eventId] || []) : []
);
// 每次 Redux state 更新,|| [] 都会创建新数组引用
```

**StockDetailPanel.js 触发频繁更新**
```javascript
useEffect(() => {
  setFilteredStocks(stocks);  // stocks 引用变化 → setState
}, [searchText, stocks]);  // stocks 是不稳定的引用
```

**无限循环链**:
1. Redux state 更新 → stocks 新引用
2. stocks 变化 → 触发 StockDetailPanel useEffect
3. useEffect 调用 setFilteredStocks → 组件重新渲染
4. 重渲染可能触发其他操作 → Redux 更新
5. 返回步骤 1,无限循环 🔁

## 解决方案
在所有 useSelector 调用中使用 shallowEqual 进行浅比较:
```javascript
import { useSelector, shallowEqual } from 'react-redux';

const stocks = useSelector(
  state => eventId ? (state.stock.eventStocksCache[eventId] || []) : [],
  shallowEqual  // 内容相同则返回旧引用,防止不必要的更新
);
```

## 修改文件
1. **useEventStocks.js** - 6 个 useSelector 添加 shallowEqual
   - stocks, quotes, historicalEvents, loading
2. **useStockMonitoring.js** - 1 个 useSelector 添加 shallowEqual
   - quotes
3. **useWatchlist.js** - 1 个 useSelector 添加 shallowEqual
   - watchlistArray

## 工作原理
shallowEqual 会比较新旧值的内容:
- 如果内容相同 → 返回旧引用 → 不触发依赖更新
- 如果内容不同 → 返回新引用 → 正常触发更新

这样可以防止因为引用变化导致的不必要重新渲染。

## 影响
-  修复无限循环错误
-  减少不必要的组件重新渲染
-  提升整体性能

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 16:30:35 +08:00

138 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Community/components/StockDetailPanel/hooks/useEventStocks.js
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { useEffect, useCallback, useMemo } from 'react';
import {
fetchEventStocks,
fetchStockQuotes,
fetchEventDetail,
fetchHistoricalEvents,
fetchChainAnalysis,
fetchExpectationScore
} from '../../../../../store/slices/stockSlice';
import { logger } from '../../../../../utils/logger';
/**
* 事件股票数据 Hook
* 封装事件相关的所有数据加载逻辑
*
* @param {string} eventId - 事件ID
* @param {string} eventTime - 事件时间
* @returns {Object} 事件数据和加载状态
*/
export const useEventStocks = (eventId, eventTime) => {
const dispatch = useDispatch();
// 从 Redux 获取数据
const stocks = useSelector(state =>
eventId ? (state.stock.eventStocksCache[eventId] || []) : [],
shallowEqual // 防止不必要的引用变化
);
const quotes = useSelector(state => state.stock.quotes, shallowEqual);
const eventDetail = useSelector(state =>
eventId ? state.stock.eventDetailsCache[eventId] : null
);
const historicalEvents = useSelector(state =>
eventId ? (state.stock.historicalEventsCache[eventId] || []) : [],
shallowEqual // 防止不必要的引用变化
);
const chainAnalysis = useSelector(state =>
eventId ? state.stock.chainAnalysisCache[eventId] : null
);
const expectationScore = useSelector(state =>
eventId ? state.stock.expectationScores[eventId] : null
);
// 加载状态
const loading = useSelector(state => state.stock.loading, shallowEqual);
// 加载所有数据
const loadAllData = useCallback(() => {
if (!eventId) {
logger.warn('useEventStocks', 'eventId 为空,跳过数据加载');
return;
}
logger.debug('useEventStocks', '开始加载事件数据', { eventId });
// 并发加载所有数据
dispatch(fetchEventStocks({ eventId }));
dispatch(fetchEventDetail({ eventId }));
dispatch(fetchHistoricalEvents({ eventId }));
dispatch(fetchChainAnalysis({ eventId }));
dispatch(fetchExpectationScore({ eventId }));
}, [dispatch, eventId]);
// 强制刷新所有数据
const refreshAllData = useCallback(() => {
if (!eventId) return;
logger.debug('useEventStocks', '强制刷新事件数据', { eventId });
dispatch(fetchEventStocks({ eventId, forceRefresh: true }));
dispatch(fetchEventDetail({ eventId, forceRefresh: true }));
dispatch(fetchHistoricalEvents({ eventId, forceRefresh: true }));
dispatch(fetchChainAnalysis({ eventId, forceRefresh: true }));
dispatch(fetchExpectationScore({ eventId }));
}, [dispatch, eventId]);
// 只刷新行情数据
const refreshQuotes = useCallback(() => {
if (stocks.length === 0) return;
const codes = stocks.map(s => s.stock_code);
logger.debug('useEventStocks', '刷新行情数据', {
stockCount: codes.length,
eventTime
});
dispatch(fetchStockQuotes({ codes, eventTime }));
}, [dispatch, stocks, eventTime]);
// 自动加载事件数据
useEffect(() => {
if (eventId) {
loadAllData();
}
}, [eventId]); // 修复:只依赖 eventId避免无限循环
// 自动加载行情数据
useEffect(() => {
if (stocks.length > 0) {
refreshQuotes();
}
}, [stocks.length, eventId]); // 注意:这里不依赖 refreshQuotes避免重复请求
// 计算股票行情合并数据
const stocksWithQuotes = useMemo(() => {
return stocks.map(stock => ({
...stock,
quote: quotes[stock.stock_code] || null
}));
}, [stocks, quotes]);
return {
// 数据
stocks,
stocksWithQuotes,
quotes,
eventDetail,
historicalEvents,
chainAnalysis,
expectationScore,
// 加载状态
loading: {
stocks: loading.stocks,
quotes: loading.quotes,
eventDetail: loading.eventDetail,
historicalEvents: loading.historicalEvents,
chainAnalysis: loading.chainAnalysis
},
// 方法
loadAllData,
refreshAllData,
refreshQuotes
};
};