React自定义Hook中Server-Sent Events无法正常工作求助
问题描述
我正在学习React,项目中原本当本地状态deviceState为"started"时,通过Server-Sent Events(SSE)数据流获取数据功能正常。但为了优化3个数据源的重复代码,编写了首个自定义Hook后,程序从未进入数据流处理流程:仅尝试一次连接,未检测到任何事件,尽管URL正确、服务器运行正常。已确认传入Hook的参数正确,且未修改数据获取与状态更新逻辑。
相关代码:
import { useEffect, useState } from "react"; export function useSSEdatastream(url: string, deviceState: 'started' | 'stopped', dataSeriesName:string, nbDataOnXaxis: number = 300, samplingRate: number = 15) { const initialData: [number, number][] = Array.from({ length: nbDataOnXaxis }, () => [0, 0]); const [dataChunk, setDataChunk] = useState<[number, number][]>(initialData); const dataSeries = [ { name: dataSeriesName, data: dataChunk }]; useEffect(() => { if (deviceState === 'started') { let source_rawdata: EventSource | null = new EventSource(url); source_rawdata.onmessage = (event) => { try { const line = JSON.parse(event.data.replace(/'/g, '"')) for (let i = 0; i < line.length - 1; i++) { const data_line = [line[i].timestamp * 1000, line[i].value] setDataChunk(prevData => [...prevData.slice(1), data_line] as [number, number][]) } } catch (error) { console.error('Error processing event:', error); } }; source_rawdata.onerror = (error) => { console.error('EventSource failed:', error); source_rawdata?.close(); source_rawdata = null; // Reconnect after a delay (e.g., 2 seconds) setTimeout(() => { source_rawdata = new EventSource(url); }, 2000); }; return () => { source_rawdata?.close(); }; } }, [deviceState]); return { dataSeries, dataChunk } }
问题原因与修复方案
1. useEffect依赖数组缺失关键变量
useEffect的依赖数组只包含了deviceState,但内部使用了url参数。当url变化时,useEffect不会重新执行,导致创建的EventSource还是旧的URL(如果初始URL有问题或者后续更新)。此外,React严格模式下会检测到依赖缺失,可能导致意外行为。
修复: 将url添加到依赖数组中:
useEffect(() => { // ... 原有逻辑 }, [deviceState, url]);
2. 重连逻辑无效
在onerror的重连代码中,setTimeout里重新赋值source_rawdata,但这个变量是当前useEffect作用域内的局部变量,新创建的EventSource没有绑定onmessage和onerror事件,等于创建了一个“哑巴”连接,不会处理任何数据事件。同时,旧的实例已经被关闭,但新实例无法被后续的cleanup函数捕获。
修复: 抽离EventSource的创建与事件绑定逻辑为独立函数,用ref保存当前实例,避免作用域问题:
import { useEffect, useState, useRef, useMemo } from "react"; export function useSSEdatastream(url: string, deviceState: 'started' | 'stopped', dataSeriesName:string, nbDataOnXaxis: number = 300, samplingRate: number = 15) { const initialData: [number, number][] = Array.from({ length: nbDataOnXaxis }, () => [0, 0]); const [dataChunk, setDataChunk] = useState<[number, number][]>(initialData); const sourceRef = useRef<EventSource | null>(null); // 抽离创建EventSource的逻辑 const createEventSource = () => { const source = new EventSource(url); source.onmessage = (event) => { try { const line = JSON.parse(event.data.replace(/'/g, '"')); for (let i = 0; i < line.length; i++) { const data_line = [line[i].timestamp * 1000, line[i].value]; setDataChunk(prevData => [...prevData.slice(1), data_line] as [number, number][]); } } catch (error) { console.error('Error processing event:', error); } }; source.onerror = (error) => { console.error('EventSource failed:', error); source.close(); sourceRef.current = null; // 重连前确认设备状态仍为started setTimeout(() => { if (deviceState === 'started') { sourceRef.current = createEventSource(); } }, 2000); }; return source; }; useEffect(() => { if (deviceState === 'started') { sourceRef.current = createEventSource(); } else { // 设备停止时关闭连接 sourceRef.current?.close(); sourceRef.current = null; } return () => { sourceRef.current?.close(); sourceRef.current = null; }; }, [deviceState, url]); // 缓存dataSeries避免不必要重渲染 const dataSeries = useMemo(() => [ { name: dataSeriesName, data: dataChunk } ], [dataSeriesName, dataChunk]); return { dataSeries, dataChunk } }
3. 额外优化:避免不必要重渲染
原代码中dataSeries每次Hook执行都会创建新的数组对象,即使dataChunk和dataSeriesName没有变化,也会导致使用该Hook的组件重渲染。用useMemo缓存dataSeries可以避免这个问题。
验证要点
- 检查浏览器控制台是否有
EventSource failed的错误日志,确认连接失败原因(比如跨域、服务器返回错误状态码)。 - 确保服务器端的SSE响应头正确:
Content-Type: text/event-stream,且禁用缓存(Cache-Control: no-cache)。 - 确认
deviceState确实切换到了started状态,可以在useEffect开头加日志验证。
内容的提问来源于stack exchange,提问作者Olivier Joubert

