Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UseEffect hook with socket.io state is not persistent in socket handlers

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

enter image description here

like image 973
Mo Hajr Avatar asked Feb 22 '19 09:02

Mo Hajr


1 Answers

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>   ); } 

Edit Executable example

like image 140
Ryan Cogswell Avatar answered Sep 21 '22 23:09

Ryan Cogswell