必须封装成 hook,因原生 websocket 是可变对象且生命周期独立于组件,仅用 usestate + useeffect 会导致重复连接、重复绑定 onmessage、卸载后 setstate 报错;核心解法是 useref 持有实例、usecallback 包裹发送/关闭逻辑、useeffect 专注初始化与清理。
直接用 new websocket() 在组件里初始化,不加封装,90% 的项目会在重连、状态同步、多次渲染导致重复连接上出问题。必须封装成 hook,但不是照抄示例代码就能跑通。
useWebSocket 为什么不能只靠 useState + useEffect
原生 WebSocket 实例是可变对象,且生命周期独立于 React 组件。如果只用 useState 存 socket,每次 setState 触发重渲染后,useEffect 可能重新执行,导致:
-
new WebSocket()被反复调用,后端看到多个连接请求 -
onmessage回调被重复绑定,一条消息触发多次处理 - 组件卸载后
ws.onclose还在执行,setState报 “Can't perform a React state update on an unmounted component”
核心解法是:用 useRef 持有 WebSocket 实例,用 useCallback 包裹发送和关闭逻辑,useEffect 里只做初始化和清理。
如何正确处理 onclose 和自动重连
WebSocket 断开不等于要立刻重连——用户手动调用 disconnect() 时,你不该自作主张重连;网络抖动或服务端重启时,才需要指数退避重试。
- 用
useRef标记是否为“手动关闭”,比如isManualClose.current = true -
onclose回调里检查这个标记,为false才启动重连 - 重连延迟建议从
1000ms开始,每次失败 ×2(即 1s → 2s → 4s → 8s),最多试 5 次 - 重连前先
clearTimeout(reconnectTimer.current),避免定时器堆积
别在 onerror 里直接重连——很多浏览器会在连接未建立成功时就触发 onerror,此时 readyState 是 0(CONNECTING),应交由 onclose 统一处理。
发送消息前必须校验 readyState
ws.send() 不会抛异常,但若 readyState !== WebSocket.OPEN,消息直接丢弃,前端毫无感知。
- 发送函数必须包裹
if (wsRef.current?.readyState === WebSocket.OPEN) - 不要依赖
isConnected状态变量——它可能滞后于真实readyState(比如onopen已触发但还没来得及setIsConnected(true)) - 对二进制或大文本消息,考虑加节流:连续快速调用
sendMessage时,可暂存到队列,等连接就绪再 flush
常见错误是把 JSON.stringify() 放在 send() 外面统一处理——后端如果要求原始字符串(非 JSON),或某些协议字段需保留数字类型(如 ID 长度超 16 位会被 JS 自动转成科学计数法),这里就会出 bug。
心跳与 token 鉴权的落地细节
生产环境 WebSocket 必须保活,否则 NAT 网关或负载均衡器会在 60s 内切断空闲连接。
- 心跳用
setInterval发{"type":"ping"},服务端回{"type":"pong"},超时未响应则主动close() - token 建议拼在 URL 查询参数里:
wsUrl = `${url}?token=${token}`,比放在 header 更兼容(浏览器 WebSocket API 不支持自定义 header) - 如果 token 有过期时间,需在重连前重新获取——不能复用初始传入的 token
- 服务端返回 400x 错误码时,
onclose的event.code是 4000+,这时不应重连,而应跳登录页
最易忽略的一点:心跳定时器必须在 ws.close() 时 clearInterval,否则即使连接断开,定时器还在后台运行,持续触发 send() 报错。











