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

React导航为何会改变原有EventSource的readyState?

根本原因
  • 核心触发点:你的useEffect依赖项设置为[prompt],按照React的effect执行规则,每次依赖项变化、effect重新执行前,一定会先运行上一轮effect返回的清理函数。你在清理函数里写了newBackendEventSource.close(),这里捕获的是上一轮effect创建的旧EventSource实例——也就是说每次你更新prompt触发导航时,代码会主动把之前创建的SSE连接关掉,对应实例的readyState自然会变成2(已关闭),这和路由导航本身没有直接关系,是你的effect清理逻辑主动关闭了旧连接。
  • 认知偏差:原生EventSource接口没有内置close事件,你绑定的close事件回调永远不会触发。标准EventSource仅支持open、message、error三个原生事件:主动调用.close()方法、连接异常断开、服务端关闭连接时,只会触发error事件,实例进入关闭状态时不会派发你自定义绑定的close事件,这就是你从未看到close回调日志的原因。
  • 设计错误:将EventSource类实例存入React state是错误实践。React state的设计目标是存储影响UI渲染、可序列化的普通数据,EventSource是持有网络连接、事件监听器的副作用资源,存入state会导致引用不稳定、重渲染时实例被意外覆盖,也无法保证事件回调中能获取到最新的状态值。
  • 隐藏bug:你在message事件回调中直接读取闭包捕获的promptsResultsArray值,这个值是effect创建瞬间的旧值,后续状态更新后回调里拿到的永远是过期数据,计算索引时很容易出现数组越界、覆盖错搜索结果的问题。
修复方案
  • 改用useRef存储所有活跃的EventSource实例,ref的.current属性不会随React重渲染重置,能稳定持有实例引用,不要把SSE实例存入React state。
  • 调整effect清理逻辑:不要在每次prompt更新时关闭旧SSE连接,仅在组件整体卸载时,遍历ref中存储的所有活跃实例,逐个关闭连接,避免内存泄漏。
  • 删除无效的close事件监听,如果需要感知单条SSE流结束,可以和后端约定自定义事件类型(比如done),在对应事件回调中关闭当前连接,并从ref的活跃实例列表中移除该实例。
  • 所有事件回调中更新状态时,使用函数式更新写法获取最新的state值,避免闭包捕获的旧值导致逻辑错误。

修正后的核心代码参考

// 用ref存储所有活跃SSE实例,不要放入state
const activeEventSources = useRef([]);
const [promptsResultsArray, setPromptsResultsArray] = useState([]);
const [prompt, setPrompt] = useState('');

// 仅在组件卸载时执行全局清理,关闭所有SSE连接
useEffect(() => {
  return () => {
    activeEventSources.current.forEach(source => {
      source.close();
    })
    activeEventSources.current = [];
  }
}, [])

// 新建搜索时创建SSE连接的逻辑
const createSSEConnection = (targetPrompt, counter) => {
  const newSource = new EventSource(
    `https://localhost:8080/generate?counter=${counter}&prompt=${targetPrompt}`,
    {withCredentials: false}
  )

  newSource.addEventListener('open', () => {
    console.log('SSE opened for prompt:', targetPrompt);
  });

  newSource.addEventListener('error', (e) => {
    // 连接关闭时readyState为2
    if (newSource.readyState === 2) {
      console.log('SSE closed for prompt:', targetPrompt);
      // 从活跃列表中移除当前实例
      activeEventSources.current = activeEventSources.current.filter(item => item !== newSource);
    } else {
      console.log('SSE error for prompt:', targetPrompt, e);
    }
  });

  // 和后端约定自定义结束事件,事件名可自行定义,此处以done为例
  newSource.addEventListener('done', (e) => {
    const data = JSON.parse(e.data);
    console.log("SSE finish data: ", data);
    newSource.close();
    activeEventSources.current = activeEventSources.current.filter(item => item !== newSource);
  });

  newSource.addEventListener('message', (e) => {
    const data = JSON.parse(e.data);
    console.log("message data: ", data);
    // 用函数式更新拿到最新的数组,不要用闭包里的旧值
    setPromptsResultsArray(prevArray => {
      const updatedArray = [...prevArray];
      const index = updatedArray.length - data.counter - 1;
      if (updatedArray[index]) {
        updatedArray[index] = {
          ...updatedArray[index],
          URIs: [data.uri],
          isLoading: false
        }
      }
      return updatedArray;
    })
  });

  // 将新实例存入ref的活跃列表
  activeEventSources.current.push(newSource);
}

const navigateToGenerateResults = (promptString) => {
  console.log('Adding new prompt results to promptsResultsArray');
  const newCounter = promptsResultsArray.length;
  const newPromptsResults = {
    isLoading: true,
    prompt: promptString,
    counter: newCounter,
    URIs: ["https://simtooreal-public.s3.amazonaws.com/white_background.png"]
  }

  setPromptsResultsArray(prevArray => [newPromptsResults, ...prevArray])
  setPrompt(promptString)
  // 直接创建SSE连接,不要放在依赖prompt的effect里避免触发误关闭
  createSSEConnection(promptString, newCounter)
  navigate(`/generate?counter=${newCounter}&prompt=${promptString}`)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 10:30:53