非严格模式下React中useEffect多次调用问题排查
问题:StompJS客户端初始化函数重复调用,useRef状态未正确保留
我希望仅执行一次
connect函数初始化StompJS客户端,但该函数被调用了4次,浏览器报错:VM1867 bundle.js:41684 WebSocket connection to 'ws://localhost:3000/chat/251/ios42gwd/websocket' failed: WebSocket is closed before the connection is established.
第4次尝试后成功连接WebSocket。我理解
useRef会在组件重渲染间保留值,但实际clientInit.current四次均为false,请问哪里出错了?
原始代码
function App() { const [userData, setUserData] = useState({ userId: null, name: "", connected: false, }); const [connected, setConnected] = useState(false); const [stompClient, setStompClient] = useState(null); const clientInit = useRef(false); //should be called only once? but called 4 times. getting clientInit.current= false 4 times too const connect = () => { console.log("inside connect"); console.log("===================================="); console.log(clientInit.current); console.log("===================================="); clientInit.current = true; const socket = new SockJS("/chat"); const tempstompClient = Stomp.over(socket); tempstompClient.connect( { "client-id": userData.userId }, () => { setConnected(true); setStompClient(tempstompClient); }, (er) => { console.error(er); } ); }; const registerUser = (e) => { e.preventDefault(); setUserData({ ...userData, connected: true }); }; const handleUser = (event) => { const { value } = event.target; const uuid = uuidv4(); setUserData({ ...userData, name: value, userId: uuid }); }; useEffect(() => { console.log("===================================="); console.log("calling connect:", clientInit.current); console.log("===================================="); if(!clientInit) connect(); return () => { console.log("Doing stuff inside cleaning up", stompClient); if (stompClient) { console.log("===================================="); console.log("disconnecting", stompClient); console.log("===================================="); stompClient.disconnect(); } }; }, []); return userData.connected ? ( <Chatbox userData={userData} stompClient={stompClient} connected={connected} /> ) : ( <Register handleUser={handleUser} handleSubmit={registerUser} /> ); } export default App;
错误原因分析
- useRef判断逻辑错误:在useEffect中写的是
if(!clientInit),但clientInit是一个Ref对象,永远不会为假值,导致每次useEffect执行都会调用connect。正确的判断应该是!clientInit.current。 - 闭包导致的状态捕获问题:useEffect的依赖数组为空,清理函数中捕获的
stompClient是初始的null,无法获取后续更新的客户端实例,可能导致无法正确断开连接。 - 连接时机过早:初始时
userData.userId为null,此时发起连接会携带无效的client-id,可能导致连接失败,后续组件重渲染又重复触发连接。
修复方案及代码
function App() { const [userData, setUserData] = useState({ userId: null, name: "", connected: false, }); const [connected, setConnected] = useState(false); const stompClientRef = useRef(null); // 改用useRef存储客户端,避免闭包问题 const clientInit = useRef(false); const connect = () => { console.log("inside connect"); console.log("===================================="); console.log(clientInit.current); console.log("===================================="); // 双重保险,防止重复执行 if (clientInit.current) return; clientInit.current = true; const socket = new SockJS("/chat"); const tempstompClient = Stomp.over(socket); tempstompClient.connect( { "client-id": userData.userId }, () => { setConnected(true); stompClientRef.current = tempstompClient; }, (er) => { console.error(er); clientInit.current = false; // 连接失败时重置状态,允许后续重试 } ); }; const registerUser = (e) => { e.preventDefault(); setUserData({ ...userData, connected: true }); }; const handleUser = (event) => { const { value } = event.target; const uuid = uuidv4(); setUserData({ ...userData, name: value, userId: uuid }); }; useEffect(() => { console.log("===================================="); console.log("calling connect:", clientInit.current); console.log("===================================="); // 仅在未初始化且有用户ID时发起连接 if (!clientInit.current && userData.userId) { connect(); } return () => { console.log("Doing stuff inside cleaning up", stompClientRef.current); if (stompClientRef.current) { console.log("===================================="); console.log("disconnecting", stompClientRef.current); console.log("===================================="); stompClientRef.current.disconnect(); stompClientRef.current = null; clientInit.current = false; // 重置状态,下次挂载可重新连接 } }; }, [userData.userId]); // 依赖userData.userId,确保用户ID生成后再连接 return userData.connected ? ( <Chatbox userData={userData} stompClient={stompClientRef.current} connected={connected} /> ) : ( <Register handleUser={handleUser} handleSubmit={registerUser} /> ); } export default App;
关键修改点
- 将
stompClient从useState改为useRef,确保清理函数能获取最新的客户端实例 - 在
connect函数开头增加重复执行判断,双重保险防止多次连接 - 修正useEffect中的判断条件为
!clientInit.current,并增加userData.userId校验,确保连接时机合理 - 将useEffect的依赖数组改为
[userData.userId],保证用户ID生成后才发起连接 - 清理函数中重置
clientInit.current和stompClientRef.current,避免下次挂载时状态混乱
内容的提问来源于stack exchange,提问作者Sabirul Mostofa
相关产品推荐
相关产品推荐

