问题
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
回答1:
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 foruseEffect
, and you would not referenceparticipants
anywhere within your effect (including all of the methods called by your message handler). If you do referenceparticipants
you'll be referencing an old version of it once the first re-render occurs. If the changes that need to occur toparticipants
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 ofparticipants
(empty), the second one would always use the second version ofparticipants
, etc. This will work and gives more flexibility in how you can use the existingparticipants
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 currentparticipants
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>
);
}
来源:https://stackoverflow.com/questions/54824036/useeffect-hook-with-socket-io-state-is-not-persistent-in-socket-handlers