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

React自定义Hook中Server-Sent Events无法正常工作求助

React自定义Hook封装SSE后无法接收数据流问题排查

问题描述

我正在学习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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 03:35:35