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