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

React useEffect仅首次正常工作、SPA重定向后失效问题排查

React useEffect 路由重定向后仅触发一次异常问题

问题现象

  • 首次加载GameComponent进入游戏房间时,所有逻辑运行正常
  • SPA路由重定向返回主页后,再次进入相同游戏流程,useEffect仅触发一次后不再生效
  • 已验证服务端可正常发送对应数据,返回的isGameReady为标准布尔值,可正常在true/false间切换
  • 已尝试将isReady、gameStep加入useEffect依赖数组,问题未解决

问题代码

完整组件代码

const GameComponent = function(props) {
    const [selectedHand, setSelectedHand] = useState('random')
    const [isGameReady, setIsGameReady] = useState(false)
    const [gameStep, setGameStep] = useState(0)
    let listenersStarted = useRef(false)
    function currentSelector(e){
        setSelectedHand(e.currentTarget.getAttribute('value'));
    }


    function getSelectedHand(value) {

        if (value === selectedHand) {
            return "box3 selected";
        }
        return "box3";
    }


    useEffect(() => {
        const serverInteractions = {

            sendChoice: ()=>{
                props.server.emit("choice", selectedHand)
            },

            startListeners: ()=>{

                props.server.emit("game status")
                if (!listenersStarted.current){
                    if (!props.server.hasListeners('game status')){
                        props.server.on('game status', async args=>{
                            await new Promise(r => setTimeout(r, 250));
                            document.getElementById('instruction').textContent = args['status']
                            setIsGameReady(args['isReady'])
                        })

                        props.server.on('game result', async ()=>{
                            await new Promise(r => setTimeout(r, 250));
                        })

                    }
                    listenersStarted.current = true
                }
            }
        }

        serverInteractions.startListeners()

    }, []);



    useEffect(() => {
        console.log(isGameReady, gameStep)
        if (isGameReady && gameStep===0){
            setGameStep(1)
        }else if(!isGameReady && gameStep>0){
            setGameStep(0)
        }
    }, [isGameReady]);
    useEffect( () => {

        const asyncFunction = async ()=>{

            if(!isGameReady && gameStep >0){
                await setGameStep(0)
            }else if (isGameReady){
                console.log(gameStep, isGameReady)
                if (isGameReady && gameStep === 1){
                    document.getElementById('instruction').textContent = ""
                    const oldGameStep = gameStep
                    await new Promise(r => setTimeout(r, 5000));

                    if (isGameReady && oldGameStep == gameStep){
                        await setGameStep(2)

                    }
                }else if(isGameReady && gameStep === 2){
                    console.log('mandando resultado')
                    //await serverInteractions.sendChoice()
                    document.getElementById('instruction').textContent = ""


                }else if(isGameReady && gameStep===3){
                    console.log(isGameReady)
                    if (isGameReady){
                        console.log(isGameReady)
                        document.getElementById('instruction').textContent = ""
                        setSelectedHand('random');
                        await new Promise(r => setTimeout(r, 5000));
                        if (isGameReady){
                            await setGameStep(0)

                        }
                    }

                }else if(isGameReady && gameStep===0){
                    await setGameStep(1)

                }

            }

        }
        asyncFunction()
    }, [gameStep]);

核心异常useEffect片段

useEffect(() => {
    console.log(isGameReady, gameStep)
    if (isGameReady && gameStep===0){
        setGameStep(1)
    }else if(!isGameReady && gameStep>0){
        setGameStep(0)
    }
}, [isGameReady]);

触发setState的事件监听片段

props.server.on('game status', async args=>{
    await new Promise(r => setTimeout(r, 250));
    document.getElementById('instruction').textContent = args['status']
    setIsGameReady(args['isReady'])
})

问题根因

核心问题出在Socket事件监听器没有在组件卸载时清理,同时listenersStarted ref的判断逻辑存在缺陷:

  1. 首次进入组件时,给全局单例的props.server(socket实例)注册了game status、game result事件监听,这些监听回调捕获的是首次组件渲染周期的setIsGameReady方法
  2. 路由跳转离开页面时,GameComponent组件卸载,但没有调用props.server.off()移除之前注册的事件监听器;同时listenersStarted是ref值,组件重新挂载时ref不会重置,仍为true
  3. 第二次进入组件时,因为listenersStarted.current === true,不会注册新的事件监听器;旧监听器持有的是上一个已卸载组件实例的setIsGameReady,调用时无法触发当前新挂载组件的state更新,表现为useEffect不触发
  4. 代码中!props.server.hasListeners('game status')判断没有实际作用:旧监听器一直挂在socket实例上,这个判断永远为false,不会重复注册,但旧监听器又绑定的是已销毁组件的闭包,无法更新当前组件状态
  5. 额外代码错误:setState是同步触发渲染的方法,不需要加await,所有await setGameStep()、await setSelectedHand()的写法均为无效写法。

修复方案

  1. 给注册socket监听的useEffect添加清理函数,组件卸载时主动移除所有事件监听
  2. 删除冗余的listenersStarted ref判断,完全依赖useEffect的生命周期管理监听器注册与销毁
  3. 不要直接通过document.getElementById操作DOM,改用React受控ref绑定DOM元素,避免内存泄漏

修复后的监听相关useEffect代码参考:

// 提前给instruction元素绑定ref
const instructionRef = useRef(null)
// 事件回调用useCallback包裹,保证引用稳定方便移除
const handleGameStatus = useCallback(async (args) => {
    await new Promise(r => setTimeout(r, 250));
    instructionRef.current.textContent = args['status']
    setIsGameReady(args['isReady'])
}, [])

const handleGameResult = useCallback(async () => {
    await new Promise(r => setTimeout(r, 250));
}, [])

useEffect(() => {
    props.server.emit("game status")
    props.server.on('game status', handleGameStatus)
    props.server.on('game result', handleGameResult)

    // 组件卸载时清理所有监听
    return () => {
        props.server.off('game status', handleGameStatus)
        props.server.off('game result', handleGameResult)
    }
}, [props.server, handleGameStatus, handleGameResult])

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 09:33:29