Warning: Cannot update during an existing state transition (such as within `render`). Render methods

解决React状态更新警告

Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

转自千问

Qwen3-Max

20:33:18

这个警告是 React 中非常经典且重要的错误:

Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.


🔥 核心原因

你在 组件的渲染过程中(render 阶段)直接调用了状态更新函数(如 setStatedispatchuseNavigate 等),这违反了 React 的“单向数据流”原则。

React 要求:渲染函数必须是“纯函数”——即给定相同的 props 和 state,总是返回相同的 JSX,且不能产生副作用(如修改状态、发起请求等)


🚫 常见错误场景 & 修复方法

❌ 场景 1:在 JSX 表达式中直接调用 setState

jsx

编辑

function MyComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {/* ❌ 错误:每次 render 都会执行 setCount */}
      {setCount(5)}
      <p>{count}</p>
    </div>
  );
}

修复:状态应在事件处理或副作用中更新,不要在 render 主体中调用


jsx

编辑

// 正确:只在点击时更新
<button onClick={() => setCount(5)}>Set to 5</button>

❌ 场景 2:在函数组件“顶层”调用状态更新(非 useEffect)

jsx

编辑

function UserProfile({ userId }) {
  const dispatch = useDispatch();

  // ❌ 错误:组件每次渲染都会 dispatch!
  dispatch(fetchUser(userId));

  return <div>...</div>;
}

修复:使用 useEffect 包裹副作用


jsx

编辑

useEffect(() => {
  dispatch(fetchUser(userId));
}, [userId, dispatch]);

❌ 场景 3:在自定义 Hook 或工具函数中隐式触发更新

jsx

编辑

// utils.js
export const getUserData = (id) => {
  store.dispatch(fetchUser(id)); // ❌ 如果 store 是全局 Redux store
};

// Component.jsx
function App() {
  const data = getUserData(123); // ❌ 在 render 中调用,触发 dispatch
  return <div>{data?.name}</div>;
}

修复

  • 将副作用移到 useEffect
  • 或使用 useCallback + 事件触发

jsx

编辑

function App() {
  const [userId] = useState(123);
  
  useEffect(() => {
    getUserData(userId); // 安全:在 effect 中调用
  }, [userId]);

  // 或通过按钮触发
  const handleLoad = () => getUserData(userId);
}

❌ 场景 4:条件性渲染中调用导航/状态更新

jsx

编辑

function App() {
  const navigate = useNavigate();
  const user = useSelector(state => state.user);

  // ❌ 错误:如果 user 为 null,立即跳转 —— 发生在 render 阶段!
  if (!user) {
    navigate('/login');
    return null;
  }

  return <Dashboard />;
}

修复:用 useEffect 处理导航逻辑


jsx

编辑

function App() {
  const navigate = useNavigate();
  const user = useSelector(state => state.user);

  useEffect(() => {
    if (!user) {
      navigate('/login');
    }
  }, [user, navigate]);

  if (!user) return <div>Loading...</div>; // 或骨架屏
  return <Dashboard />;
}

💡 注意:React Router v6 的 <Navigate> 组件是声明式的,可以在 render 中安全使用:


jsx

编辑

if (!user) return <Navigate to="/login" replace />;

❌ 场景 5:在 map / filter 等数组方法中更新状态

jsx

编辑

{items.map(item => {
  if (item.needsUpdate) {
    setItems(prev => prev.map(...)); // ❌ 千万别在 map 里 setState!
  }
  return <Item key={item.id} />;
})}

修复:提前计算好数据,不要在渲染循环中修改状态


jsx

编辑

// 在 useEffect 或事件处理中预处理
const processedItems = useMemo(() => {
  // 只做纯计算,不 setState
  return items.map(item => ({ ...item, processed: true }));
}, [items]);

✅ 通用排查步骤

  1. 看警告堆栈:点击控制台警告,定位到具体哪一行代码触发了更新。
  2. 检查该行是否在以下位置
    • 函数组件主体(非 useEffect/useCallback 内部)
    • JSX 表达式 {...} 中
    • 条件判断/循环体内(render 阶段执行的代码)
  3. 将状态更新移到
    • useEffect(响应 props/state 变化)
    • 事件处理器(如 onClick
    • useCallback 包裹的函数中

📌 关键原则总结

地方能否更新状态?替代方案
函数组件顶层(render 主体)❌ 绝对禁止→ 移入 useEffect
JSX 表达式 {...} 中❌ 禁止→ 改为变量或函数调用(但函数不能含 setState)
useEffect / 事件处理函数✅ 允许
自定义 Hook 的返回函数中✅ 允许(由用户在事件中调用)

💡 额外提示:React 18 Strict Mode 双重渲染

如果你在开发环境使用 React 18+ 的 Strict Mode,React 会故意 double-render 组件以暴露副作用问题。这会让上述错误更容易被发现!


只要确保 所有状态更新都发生在“事件”或“effect”中,而不是“render 过程”中,就能彻底解决这个警告。

如果你能提供触发警告的具体代码片段,我可以帮你精准重写!

<!-- templates/index.html --> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>🚚 智能配送看板</title> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700&family=Roboto:wght@300;400;700&display=swap" rel="stylesheet"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: linear-gradient(135deg, #0a1120, #1c2b4a); color: #e0f7fa; font-family: 'Roboto', sans-serif; overflow: hidden; height: 100vh; font-size: 14px; /* 整体字体变小 */ } .container { width: 95%; max-width: 1600px; margin: 20px auto; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.6); } header { background: #001f3f; color: #00eaff; padding: 12px 20px; font-family: 'Orbitron', sans-serif; font-size: 18px; /* 标题变小 */ font-weight: 700; letter-spacing: 1px; text-shadow: 0 0 8px rgba(0, 234, 255, 0.6); border-bottom: 1px solid #00eaff; display: flex; justify-content: space-between; align-items: center; } .title { white-space: nowrap; } .clock { font-family: 'Orbitron', monospace; font-size: 16px; color: #aaffff; text-align: right; } .scroll-container { height: calc(100vh - 120px); /* 调整高度适应新 header 和 footer */ overflow: hidden; position: relative; } .scroll-content { display: flex; flex-direction: column; transition: transform 0s; } .waybill-item { background: rgba(13, 96, 144, 0.3); margin: 6px 8px; padding: 12px; border-radius: 10px; backdrop-filter: blur(6px); border: 1px solid rgba(0, 234, 255, 0.15); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3); transition: all 0.2s; } .waybill-item:hover { transform: translateY(-1px); box-shadow: 0 5px 14px rgba(0, 234, 255, 0.25); } .driver-info { font-size: 16px; font-weight: bold; color: #00eaff; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .waybill-id { font-family: 'Orbitron', monospace; font-size: 13px; color: #aaffff; } .progress-line { display: flex; align-items: center; position: relative; } .stop-point { width: 28px; height: 28px; border-radius: 50%; margin: 0 6px; display: flex; justify-content: center; align-items: center; font-weight: bold; color: white; font-family: 'Orbitron', sans-serif; z-index: 2; font-size: 12px; } .stop-point.completed { background: radial-gradient(circle, #52c41a, #2f8514); box-shadow: 0 0 12px rgba(82, 196, 26, 0.8); } .stop-point.current { background: radial-gradient(circle, #faad14, #d48806); animation: pulse 1.5s infinite; box-shadow: 0 0 16px rgba(250, 173, 20, 0.9); transform: scale(1.05); } .stop-point.upcoming { background: radial-gradient(circle, #cccccc, #777777); color: #333; } .line { flex-grow: 1; height: 4px; background: linear-gradient(90deg, transparent, rgba(0, 234, 255, 0.4), transparent); border-radius: 2px; position: relative; overflow: hidden; } .line::before { content: ''; position: absolute; left: -100%; top: 0; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(0, 234, 255, 0.8), transparent); animation: shine 3s infinite; } @keyframes shine { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes pulse { 0%, 50% { opacity: 1; transform: scale(1.05); } 51%, 100% { opacity: 0.7; transform: scale(1); } } @keyframes slideIn { from { opacity: 0; transform: translateX(-20px) scale(0.98); } to { opacity: 1; transform: translateX(0) scale(1); } } .waybill-item.new { animation: slideIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .footer { text-align: center; padding: 10px; font-size: 12px; color: rgba(255, 255, 255, 0.7); background: rgba(0, 0, 0, 0.2); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 5px; } .empty-state { text-align: center; padding: 40px 20px; color: #aaa; font-size: 16px; } </style> </head> <body> <div class="container"> <!-- 头部:左侧标题 + 右侧时间 --> <header> <div class="title">🚀 智能物流 · 实时配送看板</div> <div class="clock" id="clock">2025-04-05 12:00:00 周六</div> </header> <div class="scroll-container" id="scrollContainer"> <div class="scroll-content" id="content"></div> </div> <!-- 底部状态栏 --> <div class="footer"> <span>📊 当前共 <strong id="totalCount">0</strong> 个配送任务</span> <span>最后更新: <span id="lastUpdate">加载中...</span></span> </div> </div> <script> const container = document.getElementById('scrollContainer'); const content = document.getElementById('content'); const lastUpdate = document.getElementById('lastUpdate'); const totalCount = document.getElementById('totalCount'); const clockEl = document.getElementById('clock'); let scrollInterval; let currentY = 0; // 更新时钟 function updateClock() { const now = new Date(); const dateStr = now.toLocaleDateString(); const timeStr = now.toLocaleTimeString(); const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; const weekday = weekdays[now.getDay()]; clockEl.textContent = `${dateStr} ${timeStr} ${weekday}`; } setInterval(updateClock, 1000); updateClock(); // 启动自动滚动(无缝循环) function startAutoScroll() { clearInterval(scrollInterval); scrollInterval = setInterval(() => { currentY += 1.2; const totalHeight = content.scrollHeight; const viewportHeight = container.clientHeight; if (currentY >= totalHeight) { // 重置到顶部,视觉上无缝衔接 content.style.transition = 'none'; content.style.transform = 'translateY(0)'; currentY = 0; // 下一帧恢复过渡动画 requestAnimationFrame(() => { requestAnimationFrame(() => { content.style.transition = 'transform 0s'; }); }); } else { content.style.transform = `translateY(-${currentY}px)`; } }, 50); } // 创建单个运单元素 function createWaybillElement(order, isNew = false) { const item = document.createElement('div'); item.className = 'waybill-item' + (isNew ? ' new' : ''); item.dataset.waybillId = order.waybill_id; const driverInfo = document.createElement('div'); driverInfo.className = 'driver-info'; const driverText = document.createElement('span'); driverText.textContent = `🚛 ${order.driver_name}`; const waybillId = document.createElement('span'); waybillId.className = 'waybill-id'; waybillId.textContent = `#${order.waybill_id}`; // ✅ 显示完整 ID driverInfo.appendChild(driverText); driverInfo.appendChild(waybillId); const progressLine = document.createElement('div'); progressLine.className = 'progress-line'; (order.stops || []).forEach((stop, index) => { const point = document.createElement('div'); point.className = 'stop-point'; if (stop.is_completed) point.classList.add('completed'); else if (stop.is_current) point.classList.add('current'); else point.classList.add('upcoming'); point.textContent = stop.sort || (index + 1); progressLine.appendChild(point); if (index < (order.stops?.length || 0) - 1) { const line = document.createElement('div'); line.className = 'line'; progressLine.appendChild(line); } }); item.appendChild(driverInfo); item.appendChild(progressLine); return item; } // 更新进度条节点(避免重复创建) function updateStopPoints(progressLine, stops) { const children = Array.from(progressLine.children); let pointIndex = 0; for (let i = 0; i < children.length; i++) { const el = children[i]; if (el.classList.contains('stop-point') && pointIndex < stops.length) { const stop = stops[pointIndex]; el.className = 'stop-point'; if (stop.is_completed) el.classList.add('completed'); else if (stop.is_current) el.classList.add('current'); else el.classList.add('upcoming'); el.textContent = stop.sort || (pointIndex + 1); pointIndex++; } } } // 主函数:获取数据并智能更新 DOM function updateData() { fetch('/api/data') .then(res => res.json()) .then(data => { // ✅ 按司机姓名排序 data.sort((a, b) => a.driver_name.localeCompare(b.driver_name)); totalCount.textContent = data.length; // 更新总数 if (!data || data.length === 0) { content.innerHTML = ''; const empty = document.createElement('div'); empty.className = 'empty-state'; empty.textContent = '📭 当前无配送中的运单'; content.appendChild(empty); return; } // 保存当前滚动位置 const computedTransform = getComputedStyle(content).transform; const matrix = new DOMMatrixReadOnly(computedTransform); const savedY = Math.abs(matrix.m42) || 0; // 构建现有元素索引 const existingMap = {}; const itemsToRemove = new Set(); for (let el of content.children) { if (el.dataset.waybillId) { existingMap[el.dataset.waybillId] = el; itemsToRemove.add(el.dataset.waybillId); } } const fragment = document.createDocumentFragment(); let hasNewItems = false; data.forEach(order => { const existing = existingMap[order.waybill_id]; if (existing) { // 更新已有元素 const driverText = existing.querySelector('.driver-info > span:first-child'); if (driverText) driverText.textContent = `🚛 ${order.driver_name}`; const waybillIdEl = existing.querySelector('.waybill-id'); if (waybillIdEl) waybillIdEl.textContent = `#${order.waybill_id}`; // 完整 ID const progressLine = existing.querySelector('.progress-line'); if (progressLine) updateStopPoints(progressLine, order.stops); fragment.appendChild(existing); itemsToRemove.delete(order.waybill_id); } else { // 新增带动画 const newItem = createWaybillElement(order, true); fragment.appendChild(newItem); hasNewItems = true; } }); // 移除已不存在的 itemsToRemove.forEach(id => { const el = document.querySelector(`[data-waybill-id="${id}"]`); if (el) el.remove(); }); // 替换内容 content.innerHTML = ''; content.appendChild(fragment); // 恢复滚动 content.style.transform = `translateY(-${savedY}px)`; lastUpdate.textContent = new Date().toLocaleTimeString(); // 清除入场动画类名 if (hasNewItems) { setTimeout(() => { document.querySelectorAll('.waybill-item.new').forEach(el => { el.classList.remove('new'); }); }, 500); } }) .catch(err => { console.error("API 请求失败:", err); }); } // 初始化 updateData(); startAutoScroll(); // 每 10 秒刷新 setInterval(updateData, 10000); </script> </body> </html> # app.py from flask import Flask, render_template, jsonify import pymysql import threading import time import logging app = Flask(__name__) # 设置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)s | %(message)s') logger = logging.getLogger(__name__) # 数据库配置 DB_CONFIG = { 'host': 'localhost', 'user': 'Gapinyc', 'password': 'Gapinyc_2025', 'database': 'gapinyc', 'charset': 'utf8mb4', 'cursorclass': pymysql.cursors.DictCursor # 使用字典游标更易读 } # 全局变量 delivery_data = [] data_lock = threading.Lock() def fetch_delivery_status(): """从数据库获取配送进度""" try: connection = pymysql.connect(**DB_CONFIG) with connection.cursor() as cursor: # 查询所有 status=4 的运单 cursor.execute("SELECT id FROM b_waybill WHERE status = 4 and isdeleted = 0 order by id") waybills = cursor.fetchall() logger.info(f"找到 {len(waybills)} 个状态为 4 的运单") data = [] for row in waybills: waybill_id = row['id'] # 查询明细(使用 DictCursor 可直接用字段名) item_sql = """ SELECT WaybillId, DriverName, OutStockOrderSort, Status FROM b_waybillitem WHERE WaybillId = %s ORDER BY OutStockOrderSort ASC """ cursor.execute(item_sql, (waybill_id,)) items = cursor.fetchall() if not items: logger.warning(f"运单 {waybill_id} 在 b_waybillitem 中无明细数据") continue # 提取司机名称(取第一条即可) driver_name = items[0]['DriverName'] or "未知司机" stops = [] current_index = None completed_count = 0 for idx, item in enumerate(items): sort_no = item['OutStockOrderSort'] status = item['Status'] # 处理 NULL 或无效值 sort_no = int(sort_no) if sort_no is not None else (idx + 1) status = int(status) if status is not None else 0 is_completed = status == 6 is_in_progress = status == 1 and current_index is None if is_in_progress: current_index = len(stops) elif is_completed: completed_count += 1 stops.append({ 'sort': sort_no, 'is_completed': is_completed, 'is_current': is_in_progress }) # 如果都没开始,默认第一个为当前 if current_index is None: current_index = 0 if stops else -1 data.append({ 'waybill_id': str(waybill_id), 'driver_name': driver_name, 'stops': sorted(stops, key=lambda x: x['sort']), 'current_index': current_index, 'total_stops': len(stops) }) return data except Exception as e: logger.error(f"数据库查询失败: {e}") return [] def update_loop(): global delivery_data while True: try: new_data = fetch_delivery_status() with data_lock: delivery_data = new_data logger.info(f"📊 成功更新数据:共 {len(new_data)} 个运单") except Exception as e: logger.error(f"❌ 更新数据出错: {e}") time.sleep(10) @app.route('/') def index(): return render_template('index.html') @app.route('/api/data') def api_data(): with data_lock: return jsonify(delivery_data) @app.route('/api/heartbeat') def heartbeat(): return jsonify({'status': 'ok', 'timestamp': int(time.time())}) if __name__ == '__main__': thread = threading.Thread(target=update_loop, daemon=True) thread.start() app.run(host='0.0.0.0', port=8089, debug=False) 提供完整的代码,解决重复的问题
最新发布
11-01
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值