未在useEffect清理函数调用WebSocket.close()致React应用崩溃原因问询
问题描述
我正在开发一个通过WebSocket连接服务器的测试应用,当不在useEffect钩子的清理部分调用WebSocket.close()时,应用会崩溃;加上这个方法后运行正常。我知道用完后该关闭连接、清理资源,但报错提示“尝试使用一个不可用或已失效的对象”,想搞清楚问题成因。
我自己分析过,怀疑和后台运行的操作有关。我理解清理操作是在渲染后执行的,不会直接影响组件渲染前的状态或渲染过程,这个理解对吗?还是我漏了什么关键点?
错误信息
Uncaught runtime errors: ERROR An attempt was made to use an object that is not, or is no longer, usable ./src/CommunicationComponent.jsx/WebSocketComponent/ </socketRef.current.onopen@http://localhost:3000/static/js/bundle.js:191:27 EventHandlerNonNull*./src/CommunicationComponent.jsx/WebSocketComponent/<@http://localhost:3000/static/js/bundle.js:189:7 commitHookEffectListMount@http://localhost:3000/static/js/bundle.js:26115:30 commitPassiveMountOnFiber@http://localhost:3000/static/js/bundle.js:27608:42 commitPassiveMountEffects_complete@http://localhost:3000/static/js/bundle.js:27580:38 commitPassiveMountEffects_begin@http://localhost:3000/static/js/bundle.js:27570:45 commitPassiveMountEffects@http://localhost:3000/static/js/bundle.js:27560:38 flushPassiveEffectsImpl@http://localhost:3000/static/js/bundle.js:29443:32 flushPassiveEffects@http://localhost:3000/static/js/bundle.js:29396:18 ./node_modules/react-dom/cjs/react-dom.development.js/commitRootImpl/<@http://localhost:3000/static/js/bundle.js:29211:13 workLoop@http://localhost:3000/static/js/bundle.js:36363:46 flushWork@http://localhost:3000/static/js/bundle.js:36341:18 performWorkUntilDeadline@http://localhost:3000/static/js/bundle.js:36578:25 EventHandlerNonNull*./node_modules/scheduler/cjs/scheduler.development.js/<@http://localhost:3000/static/js/bundle.js:36614:7 ./node_modules/scheduler/cjs/scheduler.development.js@http://localhost:3000/static/js/bundle.js:36664:5 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./node_modules/scheduler/index.js@http://localhost:3000/static/js/bundle.js:36679:20 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./node_modules/react-dom/cjs/react-dom.development.js/<@http://localhost:3000/static/js/bundle.js:6083:40 ./node_modules/react-dom/cjs/react-dom.development.js@http://localhost:3000/static/js/bundle.js:31842:5 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./node_modules/react-dom/index.js@http://localhost:3000/static/js/bundle.js:31913:20 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./node_modules/react-dom/client.js@http://localhost:3000/static/js/bundle.js:31856:28 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 fn@http://localhost:3000/static/js/bundle.js:41560:21 ./src/index.js@http://localhost:3000/static/js/bundle.js:298:93 options.factory@http://localhost:3000/static/js/bundle.js:41901:31 __webpack_require__@http://localhost:3000/static/js/bundle.js:41331:32 @http://localhost:3000/static/js/bundle.js:42544:56 @http://localhost:3000/static/js/bundle.js:42546:12
客户端代码
import React, { useEffect, useRef } from 'react'; export default function WebSocketComponent({ children }) { const socketRef = useRef(null); const isEstablish = useRef(false); // Tracks if the WebSocket connection is established const inputRef = useRef(null); // Ref for the input field useEffect(() => { if(!isEstablish.current) { socketRef.current = new WebSocket('ws://localhost:8080'); socketRef.current.onopen = () => { console.log('WebSocket connection established.'); socketRef.current.send('Hello Server!'); isEstablish.current = true; // Mark connection as established }; socketRef.current.onmessage = (event) => { console.log('Message from server:', event.data); }; socketRef.current.onerror = (error) => { console.error('WebSocket error:', error); }; socketRef.current.onclose = () => { console.log('WebSocket connection closed.'); isEstablish.current = false; // Mark connection as closed }; } // Cleanup function return () => { if (socketRef.current) { socketRef.current.close(); //it must be here, return a error in other case isEstablish.current = false; } }; }, []); // Function to send data to the server const sendToServer = () => { if (isEstablish.current && socketRef.current.readyState === WebSocket.OPEN) { const message = inputRef.current.value; console.log('Sent to server:', message); socketRef.current.send(message); // Send message only if connection is open } else { console.error('WebSocket is not open. Cannot send message.'); } }; return ( <div> <h1>WebSocket Example</h1> <input ref={inputRef} /> <button onClick={sendToServer}>Send</button> </div> ); }
问题成因与解答
1. 核心原因:WebSocket异步事件与组件生命周期脱节
当组件卸载(比如路由切换、组件被移除DOM)时,如果没主动关闭WebSocket,连接会在后台持续存在。但此时组件的socketRef虽然还持有对象引用,该WebSocket对象可能已经因为浏览器回收、连接超时或服务器主动断开变为无效状态。当WebSocket的异步回调(比如onopen、onmessage)触发时,尝试调用这个失效对象的方法,就会抛出“不可用或已失效的对象”错误。
而在清理函数中调用close(),会主动终止WebSocket连接,同时触发onclose回调,避免后续异步事件触发时访问无效对象。
2. 关于清理函数执行时机的纠正
你对清理函数执行时机的理解有误:React中useEffect的清理函数并非只在“渲染后”执行,它有两个关键触发时机:
- 组件卸载时,必然执行清理函数
- 当useEffect的依赖项变化,重新运行effect前,会先执行上一次effect的清理函数
这里的关键是:组件卸载后,WebSocket的异步回调依然可能触发,此时组件已不存在,socketRef指向的对象状态不可控,调用其方法自然会报错。
3. 代码中的潜在风险
你用isEstablish标记连接状态,但如果组件卸载后WebSocket的onopen才触发,isEstablish.current会被设为true,但此时组件已不在DOM中,若后续误触发sendToServer,就会访问无效的socketRef.current。清理函数里关闭连接并重置isEstablish,能从根源避免这种情况。
优化建议
- 新增
isMountedref,在WebSocket的所有异步回调中先检查组件是否仍挂载,避免执行无效操作:const isMounted = useRef(true); useEffect(() => { return () => { isMounted.current = false; }; }, []); // 在onopen中: socketRef.current.onopen = () => { if (!isMounted.current) return; // 后续操作 }; - 不要仅依赖
isEstablish判断连接状态,每次发送消息前必须检查socketRef.current.readyState === WebSocket.OPEN,确保连接有效。
内容的提问来源于stack exchange,提问作者Niebieski_kwiat i_kolce

