具有 socket.io 状态的 UseEffect 挂钩在套接字处理程序中不持久
UseEffect hook with socket.io state is not persistent in socket handlers
我有以下反应组件
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>
)
}
它拥有的唯一状态是调用中 participants 的哈希表,使用 useState 钩子来定义它。
然后我使用 useEffect 监听聊天室的套接字事件,只有 4 个事件
然后 之后,我将根据服务器上的执行顺序为这些事件定义 4 个回调处理程序
最后我有另一个回调函数,它被传递给列表中的每个子参与者,以便在子组件创建它的 rtcPeer 对象后,它将它发送给父级以将其设置在参与者哈希表中的参与者对象上
流程是这样的参与者加入房间 -> existingParticipants 事件被调用 -> 本地参与者被创建并添加到参与者 hashTable 然后 -> recieveVideoAnswer 和 candidate 被服务器多次发出,如您在屏幕截图中所见
第一个事件状态为空,随后的两个事件在那里,然后它再次为空,这种模式不断重复一个空状态,然后接下来的两个是正确的,我不知道状态发生了什么
困难的是,您在相互交流时遇到了几个问题,这些问题使您的故障排除变得混乱。
最大的问题是您要设置多个套接字事件处理程序。每次重新渲染,您都在调用 socket.on
而从未调用过 socket.off
。
我可以想象三种主要方法来处理这个问题:
设置单个套接字事件处理程序,并且只使用 functional updates 作为 participants
状态。使用这种方法,您将为 useEffect
使用一个空的依赖数组,并且您不会在您的效果中引用 participants
任何地方 (包括调用的所有方法)你的消息处理程序)。如果您确实引用了 participants
,您将在第一次重新渲染时引用它的旧版本。如果需要对 participants
进行的更改可以使用功能更新轻松完成,那么这可能是最简单的方法。
每次更改 participants
时设置一个新的套接字事件处理程序。为了使其正常工作,您需要删除以前的事件处理程序,否则您将拥有与渲染相同数量的事件处理程序。当你有多个事件处理程序时,创建的第一个将始终使用 participants
的第一个版本(空),第二个将始终使用 participants
的第二个版本,等等。这将起作用并在如何使用现有 participants
状态方面提供了更大的灵活性,但缺点是反复拆除和设置感觉笨拙的套接字事件处理程序。
设置单个套接字事件处理程序并使用 ref 访问当前 participants
状态。这类似于第一种方法,但添加了一个额外的效果,该效果在每次渲染时执行,以将当前 participants
状态设置为 ref,以便消息处理程序可以可靠地访问它。
无论您使用哪种方法,如果您将消息处理程序移出呈现函数并显式传入其依赖项,我认为您将更容易推理代码的作用。
第三个选项提供与第二个选项相同的灵活性,同时避免重复设置套接字事件处理程序,但在管理 participantsRef
.
时增加了一点复杂性
第三个选项的代码如下所示(我没有尝试执行这个,所以我不保证我没有小的语法问题):
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>
);
}
此外,下面是一个模拟上述代码中发生的事情的工作示例,但没有使用 socket
以清楚地显示使用 participants
与 participantsRef
之间的区别.查看控制台并单击两个按钮以查看将 participants
传递给消息处理程序的两种方式之间的区别。
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>
);
}
我有以下反应组件
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>
)
}
它拥有的唯一状态是调用中 participants 的哈希表,使用 useState 钩子来定义它。
然后我使用 useEffect 监听聊天室的套接字事件,只有 4 个事件
然后 之后,我将根据服务器上的执行顺序为这些事件定义 4 个回调处理程序
最后我有另一个回调函数,它被传递给列表中的每个子参与者,以便在子组件创建它的 rtcPeer 对象后,它将它发送给父级以将其设置在参与者哈希表中的参与者对象上
流程是这样的参与者加入房间 -> existingParticipants 事件被调用 -> 本地参与者被创建并添加到参与者 hashTable 然后 -> recieveVideoAnswer 和 candidate 被服务器多次发出,如您在屏幕截图中所见
第一个事件状态为空,随后的两个事件在那里,然后它再次为空,这种模式不断重复一个空状态,然后接下来的两个是正确的,我不知道状态发生了什么
困难的是,您在相互交流时遇到了几个问题,这些问题使您的故障排除变得混乱。
最大的问题是您要设置多个套接字事件处理程序。每次重新渲染,您都在调用 socket.on
而从未调用过 socket.off
。
我可以想象三种主要方法来处理这个问题:
设置单个套接字事件处理程序,并且只使用 functional updates 作为
participants
状态。使用这种方法,您将为useEffect
使用一个空的依赖数组,并且您不会在您的效果中引用participants
任何地方 (包括调用的所有方法)你的消息处理程序)。如果您确实引用了participants
,您将在第一次重新渲染时引用它的旧版本。如果需要对participants
进行的更改可以使用功能更新轻松完成,那么这可能是最简单的方法。每次更改
participants
时设置一个新的套接字事件处理程序。为了使其正常工作,您需要删除以前的事件处理程序,否则您将拥有与渲染相同数量的事件处理程序。当你有多个事件处理程序时,创建的第一个将始终使用participants
的第一个版本(空),第二个将始终使用participants
的第二个版本,等等。这将起作用并在如何使用现有participants
状态方面提供了更大的灵活性,但缺点是反复拆除和设置感觉笨拙的套接字事件处理程序。设置单个套接字事件处理程序并使用 ref 访问当前
participants
状态。这类似于第一种方法,但添加了一个额外的效果,该效果在每次渲染时执行,以将当前participants
状态设置为 ref,以便消息处理程序可以可靠地访问它。
无论您使用哪种方法,如果您将消息处理程序移出呈现函数并显式传入其依赖项,我认为您将更容易推理代码的作用。
第三个选项提供与第二个选项相同的灵活性,同时避免重复设置套接字事件处理程序,但在管理 participantsRef
.
第三个选项的代码如下所示(我没有尝试执行这个,所以我不保证我没有小的语法问题):
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>
);
}
此外,下面是一个模拟上述代码中发生的事情的工作示例,但没有使用 socket
以清楚地显示使用 participants
与 participantsRef
之间的区别.查看控制台并单击两个按钮以查看将 participants
传递给消息处理程序的两种方式之间的区别。
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>
);
}