React应用多标签页打开时Event Source引发数据重复问题排查
看起来你的问题核心在于新标签页打开时,原标签页的Event Source连接收到了重复数据,导致表格数据重复。虽然每个标签页是独立的React实例,各自建立了独立的SSE连接,但问题大概率出在后端的SSE推送逻辑上,结合你的前端代码,我们来一步步拆解:
可能的原因
1. 后端采用了广播式推送
如果你的Spring Boot后端在处理SSE连接时,是将数据广播给所有已连接的客户端(而不是只发送给刚建立连接的新客户端),那么当你打开新标签页建立新连接时,后端会把全量的shifts数据推送给所有在线的连接,包括原标签页的连接。这就会导致原标签页的onmessage再次触发,把所有数据追加到已有数组中,造成重复。
比如很多新手在实现Spring Boot SSE时,会用一个全局的List<SseEmitter>来存储所有连接,当有新连接加入时,遍历这个列表给所有Emitter发送数据,这就会触发广播行为。
2. 前端状态更新逻辑的追加特性
你的前端代码中,setShiftArray是用[...oldArray, JSON.parse(event.data)]的方式追加数据,这种逻辑只有在后端推送增量数据(比如新增的单个shift)时才合理。如果后端推送的是全量数据,这种追加方式必然会导致重复。
解决方案
第一步:修复后端的SSE推送逻辑
确保后端在新客户端建立连接时,只向当前新连接发送全量数据,而不是广播给所有客户端。
举个Spring Boot的正确示例(假设用SseEmitter):
@GetMapping("/shifts/getAllShifts") public SseEmitter getAllShifts(HttpServletRequest request) { SseEmitter emitter = new SseEmitter(); // 只向当前emitter发送全量数据 List<Shift> shifts = shiftService.getAllShifts(); try { emitter.send(SseEmitter.event().data(shifts)); // 如果需要后续推送增量,可以在这里维护emitter到集合,但注意只推送增量给所有客户端,而不是全量 } catch (IOException e) { emitter.completeWithError(e); } // 可以将emitter加入全局集合,用于后续增量推送,但不要在新连接时广播全量 return emitter; }
如果你的需求是后续有数据更新时,广播增量给所有客户端,那没问题,但新连接建立时只发送全量给当前客户端。
第二步:优化前端状态更新逻辑
根据后端推送的数据类型调整:
- 如果后端推送的是全量数据:直接替换数组,而不是追加
eventSource.onmessage = (event: any) => { // 直接用新的全量数据替换旧数组 setShiftArray(JSON.parse(event.data)); }; - 如果后端推送的是增量数据(单个shift):添加去重逻辑,比如根据shift的唯一ID过滤
eventSource.onmessage = (event: any) => { const newShift = JSON.parse(event.data); setShiftArray((oldArray: any) => { // 检查是否已存在相同ID的shift,不存在才添加 if (!oldArray.some(shift => shift.id === newShift.id)) { return [...oldArray, newShift]; } return oldArray; }); };
额外检查:确保Event Source连接正确关闭
你的useEffect返回了关闭连接的函数,这部分是对的,确保组件卸载时关闭连接,避免无效连接占用资源。
总结
最可能的根源是后端的广播式推送导致原标签页重复接收全量数据,先排查后端的SSE实现,再根据后端推送的数据类型调整前端的状态更新逻辑,就能解决数据重复的问题。
内容的提问来源于stack exchange,提问作者Hassan Raza

