不能只用 useState + useEffect 管 WebSocket,因原生 WebSocket 是可变对象、生命周期独立于组件,会导致重复连接、重复绑定 onmessage、卸载后 setState 报错。

直接在 useEffect 里 new WebSocket() 并用 useState 存实例,90% 的项目会在组件卸载后 setState 报错、重复绑定 onmessage、或多次触发连接——这不是写法“能跑”,而是埋了雷。
为什么不能只用 useState + useEffect 管 WebSocket
原生 WebSocket 是可变对象,生命周期独立于 React 组件。若仅靠 useState 存引用,每次重渲染都可能让 useEffect 重新执行,引发三类典型问题:
-
new WebSocket(url)被反复调用,后端看到多个并发连接请求 -
ws.onmessage回调被重复绑定,同一条消息触发多次setMessages - 组件已卸载,但
ws.onclose仍尝试setState,报错Can't perform a React state update on an unmounted component
根本解法是:用 useRef 持有唯一实例,用 useCallback 封装发送/关闭逻辑,useEffect 里只做初始化和清理。
useRef 必须持有实例,且清理函数中显式 close()
wsRef.current 是你唯一可信的连接句柄,所有操作(发消息、查状态、关连接)都必须通过它。局部变量 const ws = new WebSocket(...) 在闭包里会变成“僵尸引用”,尤其在重连或快速切换路由时极易出错。
构建 Shopify 嵌入式应用(Embedded App)的开发技能:基于 @shopify/shopify-app-react-router + React Router v7 + Admin API 2026-04。覆盖 shopifyApp 配置、authenticate.admin 的 GraphQL/REST 调用、Webhook 校验、Session 存储、订阅计费、App Bridge 导航、部署,并新增:适用场景判断、安全最佳实践、错误处理与容错、最佳实践、常见问题 FAQ、高级场景。G
-
useEffect初始化时,必须赋值wsRef.current = new WebSocket(url) - 清理函数中必须写
wsRef.current?.close(),不能只依赖组件卸载自动断开 - 发送前永远校验
wsRef.current?.readyState === WebSocket.OPEN,而不是依赖某个isConnected状态变量(它可能滞后) - 不要在
onerror里主动重连——连接未建立成功时(readyState === 0),浏览器就可能触发onerror,应统一交由onclose处理
手动重连要区分“用户关闭”和“异常断开”
自动重连不是无条件的。用户点“断开”按钮时,你不该自作主张重连;网络抖动或服务端重启才需要指数退避重试。
- 用
isManualClose = useRef(false)标记关闭意图 - 在
disconnect()函数里设isManualClose.current = true,再调ws.close() -
onclose中检查!isManualClose.current才启动重连 - 重连延迟建议从
1000ms 开始,失败后 ×2(1s → 2s → 4s → 8s),最多试 5 次 - 每次重连前先
clearTimeout(reconnectTimer.current),避免定时器堆积
接收消息必须加 try/catch 和类型守卫
event.data 可能是字符串、Blob 或 ArrayBuffer,后端偶尔发错格式就会让整个 onmessage 崩溃,UI 卡死。
- 统一用
typeof event.data === 'string' ? JSON.parse(event.data) : null - 外层必须
try/catch,捕获解析失败,不中断后续消息处理 - 高频事件(如拖拽坐标同步)需节流:用
setTimeout+clearTimeout控制发送频次,避免 1 秒内发 20 条 - 发送前别盲目
JSON.stringify()——若后端协议要求原始字符串,或字段含超长数字(JS 自动转科学计数法),这里就丢数据
真正难的不是连上,而是连得稳、断得清、发得准、收得全。很多 bug 不在连接那一瞬,而在重连逻辑是否可逆、消息队列是否积压、类型校验是否松散——这些细节不抠,上线后就是凌晨三点的告警电话。

















