带有 socket.io 状态的 UseEffect 钩子在套接字处理程序中不持久 [英] UseEffect hook with socket.io state is not persistent in socket handlers

查看:41
本文介绍了带有 socket.io 状态的 UseEffect 钩子在套接字处理程序中不持久的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下反应组件

function ConferenceRoom() {const [参与者,setParticipants] = useState({})console.log('参与者 -> ', 参与者)useEffect(() => {//消息处理程序socket.on('message', message => {console.log('收到消息:' + message.event)开关(消息.事件){案例新参与者到达":接收视频(message.userid,message.username)休息案例现有参与者":onExistingParticipants(message.userid,message.existingUsers)休息案例'receiveVideoAnswer':onReceiveVideoAnswer(message.senderid, message.sdpAnswer)休息案例候选人":addIceCandidate(message.userid, message.candidate)休息默认:休息}})返回 () =>{}}, [参与者])//套接字连接处理函数const onExistingParticipants = (userid, existingUsers) =>{console.log('onExistingParticipants Called!!!!!!')//添加本地用户常量用户 = {id:用户名,用户名:用户名,发表:真实,rtcPeer:空}setParticipants(prevParticipants => ({...prev参与者,[user.id]:用户}))existingUsers.forEach(function(element) {接收视频(element.id,element.name)})}const onReceiveVideoAnswer = (senderid, sdpAnswer) =>{console.log('参与者在接收答案 -> ', 参与者)console.log('****************')//参与者[senderid].rtcPeer.processAnswer(sdpAnswer)}const addIceCandidate = (userid,Candidate) =>{console.log('参与者在接收候选人 -> ', 参与者)console.log('****************')//参与者[userid].rtcPeer.addIceCandidate(candidate)}const receiveVideo = (userid, username) =>{console.log('收到视频呼叫!!!!')//添加远程用户常量用户 = {id:用户名,用户名:用户名,发表:假,rtcPeer:空}setParticipants(prevParticipants => ({...prev参与者,[user.id]:用户}))}//在子组件中创建rtcPeer后回调设置const setRtcPeerForUser = (userid, rtcPeer) =>{setParticipants(prevParticipants => ({...prev参与者,[userid]: { ...prevParticpants[userid], rtcPeer: rtcPeer }}))}返回 (<div id="meetingRoom">{Object.values(participants).map(participant => (<参与者键={参与者.id}参与者={参与者}房间名称={房间名称}setRtcPeerForUser={setRtcPeerForUser}sendMessage={sendMessage}/>))}

)}

它拥有的唯一状态是调用内部的 参与者 哈希表,使用 useState 钩子定义它.

然后我使用 useEffect 监听聊天室的套接字事件,只有 4 个事件

然后,我根据服务器上的执行顺序为这些事件定义了 4 个回调处理程序

最后我有另一个回调函数,它被传递给列表中的每个子参与者,以便在子组件创建其 rtcPeer 对象后,它将它发送给父组件以将其设置在参与者哈希表中的参与者对象上

流程是这样的参与者加入房间 -> existingParticipants 事件被调用 -> 本地参与者被创建并添加到参与者 hashTable 然后 -> recieveVideoAnswer 和 <如截图所示,strong>candidate 被服务器多次发出

第一个事件状态为空,随后的两个事件在那里,然后再次为空,这种模式不断重复一个空状态,然后以下两个是正确的,我不知道状态发生了什么

解决方案

这个问题的难点在于,您在相互交互时遇到了几个问题,这些问题让您的故障排除感到困惑.

最大的问题是您要设置多个套接字事件处理程序.每次重新渲染时,您都在调用 socket.on 而从未调用过 socket.off.

我可以想象出三种主要的方法来处理这个问题:

  • 设置单个套接字事件处理程序并仅使用

    I have the following react component

    function ConferencingRoom() {
        const [participants, setParticipants] = useState({})
        console.log('Participants -> ', participants)
    
        useEffect(() => {
            // messages handlers
            socket.on('message', message => {
                console.log('Message received: ' + message.event)
                switch (message.event) {
                    case 'newParticipantArrived':
                        receiveVideo(message.userid, message.username)
                        break
                    case 'existingParticipants':
                        onExistingParticipants(
                            message.userid,
                            message.existingUsers
                        )
                        break
                    case 'receiveVideoAnswer':
                        onReceiveVideoAnswer(message.senderid, message.sdpAnswer)
                        break
                    case 'candidate':
                        addIceCandidate(message.userid, message.candidate)
                        break
                    default:
                        break
                }
            })
            return () => {}
        }, [participants])
    
        // Socket Connetction handlers functions
    
        const onExistingParticipants = (userid, existingUsers) => {
            console.log('onExistingParticipants Called!!!!!')
    
            //Add local User
            const user = {
                id: userid,
                username: userName,
                published: true,
                rtcPeer: null
            }
    
            setParticipants(prevParticpants => ({
                ...prevParticpants,
                [user.id]: user
            }))
    
            existingUsers.forEach(function(element) {
                receiveVideo(element.id, element.name)
            })
        }
    
        const onReceiveVideoAnswer = (senderid, sdpAnswer) => {
            console.log('participants in Receive answer -> ', participants)
            console.log('***************')
    
            // participants[senderid].rtcPeer.processAnswer(sdpAnswer)
        }
    
        const addIceCandidate = (userid, candidate) => {
            console.log('participants in Receive canditate -> ', participants)
            console.log('***************')
            // participants[userid].rtcPeer.addIceCandidate(candidate)
        }
    
        const receiveVideo = (userid, username) => {
            console.log('Received Video Called!!!!')
            //Add remote User
            const user = {
                id: userid,
                username: username,
                published: false,
                rtcPeer: null
            }
    
            setParticipants(prevParticpants => ({
                ...prevParticpants,
                [user.id]: user
            }))
        }
    
        //Callback for setting rtcPeer after creating it in child component
        const setRtcPeerForUser = (userid, rtcPeer) => {
            setParticipants(prevParticpants => ({
                ...prevParticpants,
                [userid]: { ...prevParticpants[userid], rtcPeer: rtcPeer }
            }))
        }
    
        return (
                <div id="meetingRoom">
                    {Object.values(participants).map(participant => (
                        <Participant
                            key={participant.id}
                            participant={participant}
                            roomName={roomName}
                            setRtcPeerForUser={setRtcPeerForUser}
                            sendMessage={sendMessage}
                        />
                    ))}
                </div>
        )
    }
    

    the only state it has is a hashTable of participants inside the call using useState hook to define it.

    then I'm using useEffect to listen on the socket events for the chat room just 4 events

    then After that, I'm defining the 4 callback handlers for those events with respect to there order of execution on the server

    and last I have another callback function that gets passed to every child participant in the list so that after the child component creates its rtcPeer object it send it to the parent to set it on the participant object in the participant's hashTable

    The flow goes like this participants join the room -> existingParticipants event gets called -> local participant gets created and added to the participants hashTable then -> recieveVideoAnswer and candidate gets emitted by the server multiple time as you can see in the screenshot

    the first event the state is empty the subsequent two events its there then it's empty again and this pattern keeps repeating one empty state then the following two is correct and I have no idea what's going on with the state

    解决方案

    The difficult thing about this is that you had several issues interacting with one another that were confusing your troubleshooting.

    The biggest issue is that you are setting up multiple socket event handlers. Each re-render, you are calling socket.on without having ever called socket.off.

    There are three main approaches I can picture for how to handle this:

    • Set up a single socket event handler and only use functional updates for the participants state. With this approach, you would use an empty dependency array for useEffect, and you would not reference participants anywhere within your effect (including all of the methods called by your message handler). If you do reference participants you'll be referencing an old version of it once the first re-render occurs. If the changes that need to occur to participants can easily be done using functional updates, then this might be the simplest approach.

    • Set up a new socket event handler with each change to participants. In order for this to work correctly, you need to remove the previous event handler otherwise you will have the same number of event handlers as renders. When you have multiple event handlers, the first one that was created would always use the first version of participants (empty), the second one would always use the second version of participants, etc. This will work and gives more flexibility in how you can use the existing participants state, but has the down side of repeatedly tearing down and setting up socket event handlers which feels clunky.

    • Set up a single socket event handler and use a ref to get access to the current participants state. This is similar to the first approach, but adds an additional effect that executes on every render to set the current participants state into a ref so that it can be accessed reliably by the message handler.

    Whichever approach you use, I think you will have an easier time reasoning about what the code is doing if you move your message handler out of your rendering function and pass in its dependencies explicitly.

    The third option provides the same kind of flexibility as the second option while avoiding repeated setup of the socket event handler, but adds a little bit of complexity with managing the participantsRef.

    Here's what the code would look like with the third option (I haven't tried to execute this, so I make no guarantees that I don't have minor syntax issues):

    const messageHandler = (message, participants, setParticipants) => {
      console.log('Message received: ' + message.event);
    
      const onExistingParticipants = (userid, existingUsers) => {
        console.log('onExistingParticipants Called!!!!!');
    
        //Add local User
        const user = {
          id: userid,
          username: userName,
          published: true,
          rtcPeer: null
        };
    
        setParticipants({
          ...participants,
          [user.id]: user
        });
    
        existingUsers.forEach(function (element) {
          receiveVideo(element.id, element.name)
        })
      };
    
      const onReceiveVideoAnswer = (senderid, sdpAnswer) => {
        console.log('participants in Receive answer -> ', participants);
        console.log('***************')
    
        // participants[senderid].rtcPeer.processAnswer(sdpAnswer)
      };
    
      const addIceCandidate = (userid, candidate) => {
        console.log('participants in Receive canditate -> ', participants);
        console.log('***************');
        // participants[userid].rtcPeer.addIceCandidate(candidate)
      };
    
      const receiveVideo = (userid, username) => {
        console.log('Received Video Called!!!!');
        //Add remote User
        const user = {
          id: userid,
          username: username,
          published: false,
          rtcPeer: null
        };
    
        setParticipants({
          ...participants,
          [user.id]: user
        });
      };
    
      //Callback for setting rtcPeer after creating it in child component
      const setRtcPeerForUser = (userid, rtcPeer) => {
        setParticipants({
          ...participants,
          [userid]: {...participants[userid], rtcPeer: rtcPeer}
        });
      };
    
      switch (message.event) {
        case 'newParticipantArrived':
          receiveVideo(message.userid, message.username);
          break;
        case 'existingParticipants':
          onExistingParticipants(
              message.userid,
              message.existingUsers
          );
          break;
        case 'receiveVideoAnswer':
          onReceiveVideoAnswer(message.senderid, message.sdpAnswer);
          break;
        case 'candidate':
          addIceCandidate(message.userid, message.candidate);
          break;
        default:
          break;
      }
    };
    
    function ConferencingRoom() {
      const [participants, setParticipants] = React.useState({});
      console.log('Participants -> ', participants);
        const participantsRef = React.useRef(participants);
        React.useEffect(() => {
            // This effect executes on every render (no dependency array specified).
            // Any change to the "participants" state will trigger a re-render
            // which will then cause this effect to capture the current "participants"
            // value in "participantsRef.current".
            participantsRef.current = participants;
        });
    
      React.useEffect(() => {
        // This effect only executes on the initial render so that we aren't setting
        // up the socket repeatedly. This means it can't reliably refer to "participants"
        // because once "setParticipants" is called this would be looking at a stale
        // "participants" reference (it would forever see the initial value of the
        // "participants" state since it isn't in the dependency array).
        // "participantsRef", on the other hand, will be stable across re-renders and 
        // "participantsRef.current" successfully provides the up-to-date value of 
        // "participants" (due to the other effect updating the ref).
        const handler = (message) => {messageHandler(message, participantsRef.current, setParticipants)};
        socket.on('message', handler);
        return () => {
          socket.off('message', handler);
        }
      }, []);
    
      return (
          <div id="meetingRoom">
            {Object.values(participants).map(participant => (
                <Participant
                    key={participant.id}
                    participant={participant}
                    roomName={roomName}
                    setRtcPeerForUser={setRtcPeerForUser}
                    sendMessage={sendMessage}
                />
            ))}
          </div>
      );
    }
    

    Also, below is a working example simulating what is happening in the above code, but without using socket in order to show clearly the difference between using participants vs. participantsRef. Watch the console and click the two buttons to see the difference between the two ways of passing participants to the message handler.

    import React from "react";
    
    const messageHandler = (participantsFromRef, staleParticipants) => {
      console.log(
        "participantsFromRef",
        participantsFromRef,
        "staleParticipants",
        staleParticipants
      );
    };
    
    export default function ConferencingRoom() {
      const [participants, setParticipants] = React.useState(1);
      const participantsRef = React.useRef(participants);
      const handlerRef = React.useRef();
      React.useEffect(() => {
        participantsRef.current = participants;
      });
    
      React.useEffect(() => {
        handlerRef.current = message => {
          // eslint will complain about "participants" since it isn't in the
          // dependency array.
          messageHandler(participantsRef.current, participants);
        };
      }, []);
    
      return (
        <div id="meetingRoom">
          Participants: {participants}
          <br />
          <button onClick={() => setParticipants(prev => prev + 1)}>
            Change Participants
          </button>
          <button onClick={() => handlerRef.current()}>Send message</button>
        </div>
      );
    }
    

    这篇关于带有 socket.io 状态的 UseEffect 钩子在套接字处理程序中不持久的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆