React、Next.js开发WebSocket日志应用时state始终返回初始值
核心问题成因
闭包捕获的过期state引用
useEffect依赖数组为空,意味着该逻辑仅在组件第一次挂载时执行一次。此时绑定到WebSocket实例的onWebSocketMessage回调,捕获到的是首次渲染周期下的固定变量引用——即空数组状态的eventLog、初始值0的updateFlag。
哪怕后续组件因state更新触发重渲染,已经绑定到WebSocket实例上的消息回调永远不会更新内部捕获的变量引用,始终读取第一次渲染时的初始state值。这就是为什么拼接日志时永远只显示单条新消息、console.log(updateFlag)永远输出0的根本原因。
组件作用域普通变量的引用失效
写在组件内部的let socket = null,每次组件重渲染都会被重新初始化为null,和已经创建完成的WebSocket实例完全脱钩。这种写法既会丢失对WebSocket实例的控制,也无法在组件卸载时正确关闭连接,存在内存泄漏风险。
具体修复方案
- 采用函数式state更新绕开闭包问题
调用setState时传入回调函数,回调入参是React官方维护的当前最新state值,不依赖外层闭包捕获的旧变量,从根源解决读不到最新state的问题:- 替换
setEventLog逻辑:不再引用外层的eventLog变量拼接日志,直接基于React传入的最新日志数组做拼接 - 替换
setUpdateFlag逻辑:不再引用外层的updateFlag计算递增量,直接基于React传入的最新标记值计算新值
- 替换
- 用
useRef持久化WebSocket实例引用useRef创建的对象在组件全生命周期内引用固定,不会因重渲染被重置,适合存储WebSocket这类需要跨渲染周期访问的实例。 - 补充副作用清理逻辑
在useEffect的返回函数中增加连接关闭逻辑,避免组件卸载、路由切换后旧的WebSocket连接持续接收消息,触发无效state更新。 - 优化列表渲染key值
当前使用数组索引index作为日志列表的key,在新日志始终插入数组头部的场景下会出现渲染错位,建议为每条日志绑定唯一标识作为key值。
修复后的参考代码
import React, { useState, useEffect, useRef } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; export default function Home() { // 持久化WebSocket实例引用,避免重渲染时被重置 const socketRef = useRef(null); const [eventLog, setEventLog] = useState([]); const [updateFlag, setUpdateFlag] = useState(0); const onWebSocketMessage = (e) => { const data = JSON.parse(e.data); const newLogItem = { title: data.title, icon: data.icon, body: data.body, date: new Date(), }; // 函数式更新:prevLog为React维护的最新日志数组,不受闭包影响 setEventLog(prevLog => [newLogItem, ...prevLog]); // 函数式更新:prevFlag为最新的updateFlag值,基于当前值计算递增量 setUpdateFlag(prevFlag => { // 如需打印最新计数,可在更新回调内读取值 console.log('已接收消息数:', prevFlag + 1); return prevFlag + 1; }); }; const connectWebSocket = (token) => { socketRef.current = new WebSocket('wss://localhost/' + token); socketRef.current.onmessage = onWebSocketMessage; }; useEffect(() => { const token = localStorage.getItem('token'); connectWebSocket(token); // 组件卸载时清理WebSocket连接 return () => { if (socketRef.current) { socketRef.current.onmessage = null; socketRef.current.close(); socketRef.current = null; } }; }, []); const eventLogHtml = eventLog.map((obj, index) => ( // 生产环境建议替换为日志项唯一ID作为key <div key={index}> <h2>{obj.title}</h2> <div> <img src={obj.icon} style={{ width: '8rem' }} /> <span> <p>{obj.body}</p> <p>{obj.date.toString()}</p> </span> </div> </div> )); return ( <Container> <Row> <Col> <h1>Event log</h1> {eventLogHtml} </Col> </Row> </Container> ); }
注意:如果需要追踪
updateFlag的实时值做其他业务逻辑,建议单独通过useEffect监听updateFlag的变化执行对应操作,不要在消息回调里直接读取外层的updateFlag变量,否则依然会读到闭包捕获的旧值。
内容的提问来源于stack exchange,提问作者Kaoru
相关产品推荐
相关产品推荐

