news 2026/8/12 19:16:33

React 用 useEffect 订阅 WebSocket:连接管理、自动重连与卸载清理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React 用 useEffect 订阅 WebSocket:连接管理、自动重连与卸载清理

React 用 useEffect 订阅 WebSocket:连接管理、自动重连与卸载清理

写实时功能时,很多人第一版 WebSocket 代码是这样的:组件里new WebSocket(url),onmessagesetState。跑起来能收到消息,看着没问题。但上线后就会遇到一连串诡异现象:切个路由回来发现开了两条连接、后端重启后前端再也收不到消息、控制台不停报「Can’t perform a React state update on an unmounted component」。

这些都不是玄学,而是 WebSocket 的生命周期没有和 React 组件的生命周期对齐。这篇就把连接的建立、清理、重连一步步做对。

朴素写法:为什么会开出两条连接

function Chat({ roomId }) { const [messages, setMessages] = useState([]); useEffect(() => { const ws = new WebSocket(`wss://example.com/room/${roomId}`); ws.onmessage = (e) => { setMessages((prev) => [...prev, JSON.parse(e.data)]); }; // 没有返回清理函数 }, [roomId]); return <MessageList items={messages} />; }

这段代码有两个致命问题:

第一,没有清理函数roomId变化时,React 会重新执行 effect,建立新连接,但旧连接从没关过。切三次房间就有三条连接同时在收消息,messages里全是重复数据。

第二,在 React 18 严格模式(StrictMode)下,开发环境的 effect 会执行两次。这是 React 故意的:它挂载组件、卸载、再挂载,用来暴露你没写清理逻辑的 bug。没有 cleanup 的 WebSocket 代码在这里会立刻现形——你会看到两条连接。

正确写法:cleanup 里关闭连接

effect 的返回值就是清理函数,它会在「下次 effect 执行前」和「组件卸载时」被调用。把关闭连接放进去:

function Chat({ roomId }) { const [messages, setMessages] = useState([]); useEffect(() => { const ws = new WebSocket(`wss://example.com/room/${roomId}`); ws.onmessage = (e) => { setMessages((prev) => [...prev, JSON.parse(e.data)]); }; // 关键:切换 roomId 或卸载时,先把旧连接关掉 return () => { ws.close(); }; }, [roomId]); return <MessageList items={messages} />; }

现在切房间时,旧连接先close,再建新的;组件卸载时连接也会被回收。StrictMode 的双次挂载也不再泄漏——第一次挂载建的连接会在卸载时关掉。

但还有个隐藏的坑:ws.close()是异步的,如果连接还在CONNECTING状态就调用 close,某些浏览器会先打开再关闭,onmessage仍可能触发一次。所以清理时最好把回调也摘掉:

return () => { ws.onmessage = null; // 防止关闭过程中还触发 setState ws.close(); };

这样即使连接在关闭窗口期收到一帧数据,也不会走到已卸载组件的setState,那条经典警告就消失了。

自动重连:后端重启也能自愈

真实网络里连接一定会断:后端发版、Nginx 超时、用户网络抖动。断了要自动重连,而且要用指数退避,别断了就疯狂重连把服务器打垮。

重连逻辑放 effect 里很容易和清理逻辑纠缠,推荐抽成一个自定义 Hook。这里的关键是用一个shouldReconnect标志位区分「主动关闭」和「异常断开」:

import { useEffect, useRef, useState } from "react"; function useWebSocket(url) { const [status, setStatus] = useState("connecting"); const [lastMessage, setLastMessage] = useState(null); const wsRef = useRef(null); useEffect(() => { let retry = 0; let timer = null; // 主动清理时置 false,阻止 onclose 里再触发重连 let shouldReconnect = true; const connect = () => { const ws = new WebSocket(url); wsRef.current = ws; ws.onopen = () => { retry = 0; // 连上就把退避计数清零 setStatus("open"); }; ws.onmessage = (e) => setLastMessage(JSON.parse(e.data)); ws.onclose = () => { setStatus("closed"); if (!shouldReconnect) return; // 指数退避:1s、2s、4s…最多 30s,避免雪崩 const delay = Math.min(1000 * 2 ** retry, 30000); retry += 1; timer = setTimeout(connect, delay); }; ws.onerror = () => ws.close(); // 出错统一走 onclose 重连 }; connect(); return () => { shouldReconnect = false; // 卸载是主动行为,别再重连 clearTimeout(timer); if (wsRef.current) { wsRef.current.onmessage = null; wsRef.current.close(); } }; }, [url]); return { status, lastMessage }; }

用起来就干净了:

function Chat({ roomId }) { const { status, lastMessage } = useWebSocket( `wss://example.com/room/${roomId}` ); const [messages, setMessages] = useState([]); useEffect(() => { if (lastMessage) setMessages((prev) => [...prev, lastMessage]); }, [lastMessage]); return ( <> {status === "closed" && <Banner>连接断开,重连中…</Banner>} <MessageList items={messages} /> </> ); }

shouldReconnect这个标志位是重点:没有它,组件卸载时的主动close会触发onclose,进而又调度一次重连,setTimeout在组件死后还在跑,重连成功又建了一条没人管的连接。有了它,主动关闭就是真的关闭。

发消息:别在 render 里直接调 ws.send

想发消息时,不能直接引用 effect 内部的ws(闭包里拿不到)。正确做法是把send从 Hook 暴露出去,内部读wsRef:

const send = useCallback((data) => { const ws = wsRef.current; // 只有 OPEN 状态才能发,重连期间发会抛异常 if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(data)); } }, []); return { status, lastMessage, send };

判断readyState === OPEN很关键:重连窗口期连接处于CONNECTING,此时send会直接抛InvalidStateError。生产环境里更完善的做法是维护一个发送队列,连接恢复后再 flush,但先把状态判断加上就能挡掉绝大多数崩溃。

小结

  • 必须写 cleanup:effect 返回函数里ws.close(),否则切换依赖和 StrictMode 双挂载都会泄漏连接。
  • 清理时先摘onmessage:防止关闭窗口期的数据帧触发已卸载组件的 setState。
  • 重连用指数退避 +shouldReconnect标志:区分主动关闭与异常断开,避免卸载后僵尸重连。
  • onopen里重置退避计数,onerror统一走onclose收口。
  • 发消息前判readyState === OPEN,重连期间发送会抛异常。

一句话记忆:WebSocket 的生命周期要挂在 effect 的 cleanup 上,连接是你开的,关也得你亲手关干净。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/12 19:14:43

第15篇:loop 步骤的循环调用与结果合并机制

第15篇:loop 步骤的循环调用与结果合并机制 工作流引擎的七种步骤类型中,loop 是最复杂也最强大的一个。它能遍历一个列表,对每个元素调用不同的 Skill,把所有结果合并成一个扁平列表。“查询预算小于 200 的项目的全部附件”——这个需求需要先查所有项目、过滤预算、再对…

作者头像 李华
网站建设 2026/8/12 19:12:44

终极AI图层分离指南:3分钟学会layerdivider智能分层技术

终极AI图层分离指南&#xff1a;3分钟学会layerdivider智能分层技术 【免费下载链接】layerdivider A tool to divide a single illustration into a layered structure. 项目地址: https://gitcode.com/gh_mirrors/la/layerdivider 你是否曾花费数小时手动分离插画图层…

作者头像 李华
网站建设 2026/8/12 19:11:59

菜价涨了,肉价跌了,菜市场里藏着什么

菜价涨了&#xff0c;肉价跌了&#xff0c;菜市场里藏着什么今早七点半&#xff0c;我在小区门口那家菜摊前站了足足五分钟。摊主老周蹲在泡沫箱后面&#xff0c;手里攥着一把空心菜&#xff0c;嘴里念叨着"八块八块"&#xff0c;眼神却飘向别处。旁边一位大姐拿起又…

作者头像 李华
网站建设 2026/8/12 19:11:44

测试工程师如何构建系统性学习体系:从工具到思维的全面升级

1. 从“测试”到“测试的学习”&#xff1a;一个被忽视的认知升级 最近在和一些刚入行的朋友聊天&#xff0c;发现一个挺有意思的现象。很多人&#xff0c;包括我自己刚入行那会儿&#xff0c;都容易把“做测试”和“学习测试”混为一谈。前者是执行&#xff0c;是任务&#xf…

作者头像 李华
网站建设 2026/8/12 19:07:01

深入解析JVM类加载与JIT编译原理及优化实践

1. 类加载与即时编译的核心原理剖析 在Java虚拟机(JVM)的执行过程中&#xff0c;类加载机制与即时编译(JIT)技术是影响性能表现的两大关键因素。作为一名长期从事JVM调优的开发者&#xff0c;我发现很多同行虽然能熟练使用Java语言&#xff0c;但对底层运行机制的理解往往停留在…

作者头像 李华
网站建设 2026/8/12 19:06:54

Navicat数据库管理工具:从入门到精通,图形化操作与高效开发实战

1. 从零到一&#xff1a;Navicat到底是什么&#xff0c;以及为什么你需要它 如果你刚接触数据库开发或者运维&#xff0c;听到Navicat这个名字可能会有点懵。简单来说&#xff0c;Navicat是一个图形化的数据库管理工具&#xff0c;它就像数据库的“瑞士军刀”。想象一下&#x…

作者头像 李华