You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中stomp.js订阅回调内调用setState无法更新状态

问题根因

这是React函数组件结合长连接场景非常典型的闭包过期问题,结合你的代码逻辑,具体触发原因如下:

  • 初始化socket的useEffect设置了空依赖数组,仅在组件首次挂载时执行,subscribe方法绑定的回调函数是首次渲染周期生成的闭包版本
  • React 18开发环境默认开启严格模式,会自动执行「挂载→模拟卸载→重新挂载」的流程:第一次挂载时执行initSocket创建stomp实例,还没等连接完成就提前把实例存入了appContext;随后组件被模拟卸载,第一次绑定的回调对应的组件实例已经被销毁
  • 组件第二次挂载时,判断!appContext.socket为false(第一次创建的实例已经存在context里),不会重新执行初始化和订阅绑定。后续长连接收到消息时,触发的是已经销毁的旧组件实例绑定的回调,回调里的setIsIncomingCall调用会被React直接拦截,不会触发当前组件的状态更新——这也是为什么回调里的日志、普通计算逻辑能正常执行,但页面状态永远不变的核心原因
  • 就算关闭严格模式,只要组件发生卸载重挂载(比如路由跳转后返回、父组件重渲染触发子组件重建),因为socket实例存在全局context中不会重建,绑定的永远是旧实例的回调,一样会复现这个问题。你之前尝试的useCallback、箭头函数包裹等方案没有生效,本质都是没有解决「回调引用永远指向旧组件实例」的核心问题。
修复方案

按以下三点调整代码即可解决:

  1. 用useRef存储最新的消息处理回调,保证长连接消息触发时永远能拿到当前组件实例的最新逻辑,不需要关心闭包依赖问题
  2. 调整stompClient存入context的时机,等连接成功后再存入全局context,避免未完成初始化的实例被异常复用
  3. 增加组件卸载时的清理逻辑,断开旧连接、清空context中存储的实例,避免旧订阅残留

调整后的核心代码如下:

export default function HomePage() {

    // ...其他状态定义
    const [isIncomingCall, setIsIncomingCall] = useState(false)
    // 新增ref存储最新的来电处理回调
    const incomingCallHandlerRef = useRef(null)

    // 每次组件渲染都更新ref中存储的最新回调
    useEffect(() => {
        incomingCallHandlerRef.current = (data) => {
            const state = JSON.parse(data.body.toLowerCase());
            setIsIncomingCall(state)
        }
    })

    function initSocket() {
        const socket = new SockJS(`${process.env.API_URL}/ws`);
        const stompClient = over(socket);
        stompClient.connect(
            {},
            () => onSocketConnected(stompClient),
            onSocketError
        )
    }

    function onSocketConnected(stompClient) {
        // 连接成功后再将实例存入全局context
        appContext.setSocket(stompClient)
        const options = {
            accessToken: cookies['logged-in'],
        };

        stompClient.send("/app/connect", {}, JSON.stringify(options));
        stompClient.subscribe(`/user/search/complete`, handleSearchComplete)
        stompClient.subscribe(`/user/search/failed`, handleSearchError)
        // 订阅时通过ref调用最新的处理函数,永远不会拿到过期闭包
        stompClient.subscribe(`/user/call/incoming`, (data) => {
            incomingCallHandlerRef.current?.(data)
        })
    }

    useEffect(() => {
        if (!appContext.socket && cookies['logged-in']) {
            initSocket()
        }
        // 组件卸载时清理连接,避免旧实例残留
        return () => {
            if (appContext.socket) {
                appContext.socket.disconnect()
                appContext.setSocket(null)
            }
        }
    }, [])

    return (
        <>
            <AnimatePresence>
                {
                    isIncomingCall && <CallModal
                        onAccept={acceptIncomingCall}
                        onReject={rejectIncomingCall}
                    />
                }
            </AnimatePresence>
            {/* 其他页面代码 */}
        </>
    )
}

额外注意点

  • 建议在回调里增加日志确认JSON.parse(data.body.toLowerCase())的解析结果符合预期,避免因为后端返回格式问题导致setIsIncomingCall传入的值一直和初始值一致,看起来像是状态没有更新
  • 如果socket不需要跨多个路由/组件全局复用,最好不要把stompClient存在appContext中,直接在当前组件用ref管理实例生命周期即可,能大幅减少全局实例复用带来的闭包、残留订阅问题

内容的提问来源于stack exchange,提问作者Nikitorius

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 05:15:31