React中直接用useEffect+new WebSocket()易导致内存泄漏、重复连接或InvalidStateError;必须用useRef保存实例,清理时显式close(),发消息前校验readyState,并建议用useSWRSubscription替代手写逻辑。

直接用 useEffect + new WebSocket() 启动连接,不加状态引用保护和重连逻辑,90% 的 React WebSocket 应用会在页面切换、组件卸载或网络抖动时出错——不是收不到消息,就是重复连接、内存泄漏、ws.send() 报 InvalidStateError。
useRef 保存 WebSocket 实例是刚需,不是可选
WebSocket 对象不能存在 useState 或闭包变量里。每次组件重渲染都会生成新函数,如果事件回调(如 onmessage)里直接调用 setState,又没用 useCallback + useRef 锁定实例,就会触发旧连接还在运行、新连接又建了一次的竞态问题。
-
wsRef.current必须在useEffect内赋值,且清理函数中显式调用wsRef.current?.close() - 发送消息必须通过
wsRef.current?.send(),绝不能用局部变量ws—— 它可能已是上一轮渲染的“僵尸引用” - 连接状态(
connecting/open/closing)建议用useState管理,但判断是否可发消息,必须查wsRef.current?.readyState === WebSocket.OPEN
useSWRSubscription 是比手写 useEffect 更稳的订阅方案
如果你的数据源本质是“持续推送+按需消费”,比如实时行情、日志流、协作光标,useSWRSubscription 比裸 WebSocket Hook 更适合:它内置连接生命周期绑定、错误重试退避、自动取消未挂载组件的监听,且与 SWR 缓存层天然兼容。
- 不要在
useSWRSubscription的回调里手动new WebSocket()后不返回清理函数——这会导致连接永不释放 - 推送数据必须调用
next(null, data);出错必须调用next(error),否则 SWR 不会触发error状态更新 - URL 变更会自动触发重新订阅,但注意:若服务端不支持多路复用,频繁切换 key 可能导致连接风暴
消息收发必须做类型守卫和防抖校验
真实环境里,WebSocket 收到的 event.data 可能是字符串、Blob、甚至 ArrayBuffer;服务端偶尔发错格式、前端误调多次 send,都会让 UI 崩溃或卡死。
- 接收时统一用
typeof event.data === 'string' ? JSON.parse(event.data) : null,并用try/catch包裹,避免解析失败中断整个onmessage - 发送前检查
wsRef.current?.readyState === WebSocket.OPEN,否则跳过并记录 warn 日志,而不是抛异常 - 高频事件(如拖拽坐标、输入框实时同步)必须节流,例如用
setTimeout+clearTimeout防止 1 秒内发 20 条相同消息
React Router 场景下,路由切换时 WebSocket 连接不该销毁
很多开发者在每个页面组件里独立建 WebSocket,结果用户从 /dashboard 切到 /chat,旧连接被 useEffect 清理掉,再切回来又新建——不仅浪费握手开销,还丢失中间消息。
- 把 WebSocket 实例提升到布局组件(如
AppLayout)或自定义 Provider 中,用 Context 或 Zustand 管理全局连接 - 用
useLocation监听路径变化,只在必要时广播当前路由(如协作场景),而非重建连接 - 若不同路由需不同消息频道(如 chat vs. notifications),应在已建立的连接上用消息 type 字段路由,而不是开多个 ws
最常被忽略的一点:WebSocket 关闭后,onclose 回调里的 event.code 和 event.reason 是唯一能区分“用户主动退出”和“网络闪断”的依据;不做这个判断,就无法决定该静默重连还是提示用户检查网络。


















