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

Next.js+Zustand+SignalR并发更新异常问题求助

问题分析

你遇到的核心问题是:SignalR并发推送消息时,Zustand的状态更新仅首个生效,其余无效果。原因在于非React事件循环中的并发状态更新未被正确批量处理,且Zustand的set函数基于调用时刻的状态快照执行,多个并发更新会互相覆盖;此外,原生的不可变数组更新写法在高并发场景下无法保证基于最新状态修改。

可行解决方案

方案1:使用Immer简化并发状态更新

Zustand原生支持Immer集成,通过Immer的可变语法处理状态更新,底层会自动保证每次修改基于最新状态,避免并发覆盖问题。

修改你的Zustand store:

import { create } from 'zustand';
import { produce } from 'immer';

const useCallStore = create((set) => ({
  calls: [],
  handleCall: (update, id) => {
    set(produce((state) => {
      // 找到对应ID的服务项索引
      const targetIndex = state.calls.findIndex(call => call.id === id);
      if (targetIndex !== -1) {
        // 直接替换对应项(或按需合并字段)
        state.calls[targetIndex] = update;
      }
    }));
  }
}));

这种写法无需手动处理不可变数组操作,Immer会自动转换为安全的不可变更新,同时确保并发场景下每个修改都基于最新状态。

方案2:强制批量处理非React事件中的更新

SignalR的回调属于非React事件循环,Zustand默认不会批量处理这类更新,导致每个set调用独立触发重渲染,且状态更新基于旧快照。可以用React的unstable_batchedUpdates将更新批量包裹,确保状态正确合并。

修改SignalR监听代码:

import { unstable_batchedUpdates } from 'react-dom';

hubConnection.on(TRIPSTART, (stage, trip) => {
  console.log(TRIPSTART, stage, trip);
  const updateTrip = tripConvert(trip);
  if (trip.price == null) {
    toast.error(`Call with ID ${trip.id} doesn't have a price.`, { autoClose: false });
  }
  
  // 批量处理状态更新
  unstable_batchedUpdates(() => {
    handleCall(updateTrip, trip.tripID);
  });
});

这种方式适用于不想引入Immer的场景,通过批量更新确保多个并发的状态修改被正确应用。

方案3:优化原生状态更新逻辑(不推荐)

如果你坚持使用原生不可变更新,可以确保每次更新都基于最新状态(但写法繁琐,并发场景下仍有风险):

handleCall: (update, id) => {
  set((state) => ({
    calls: state.calls.map(call => 
      call.id === id ? { ...call, ...update } : call
    ),
  }));
}

注:此写法仅当update是部分字段更新时有效,若直接替换整个对象,和原写法无本质区别,仍可能出现并发覆盖。

验证建议
  • 测试时可以在handleCall中打印当前state.calls的状态,确认每次更新是否基于最新数据;
  • 检查trip.tripID与call.id的匹配关系,确保所有更新请求的ID都存在于calls数组中。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 06:35:26