带有 socket.io 状态的 UseEffect 钩子在套接字处理程序中不持久

Posted

技术标签:

【中文标题】带有 socket.io 状态的 UseEffect 钩子在套接字处理程序中不持久【英文标题】:UseEffect hook with socket.io state is not persistent in socket handlers 【发布时间】:2019-07-16 08:49:51 【问题描述】:

我有以下反应组件

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

它拥有的唯一状态是调用内部的 参与者 的哈希表,使用 useState 挂钩对其进行定义。

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

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

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

流程如下,参与者加入房间 -> existingParticipants 事件被调用 -> 本地参与者被创建并添加到参与者哈希表中 -> recieveVideoAnswer 和 candidate 被服务器多次发出,如您在屏幕截图中看到的那样

第一个事件的状态是空的,随后的两个事件是空的,然后它又是空的,这种模式不断重复一个空状态,然后接下来的两个是正确的,我不知道状态发生了什么

【问题讨论】:

您没有将空数组作为useEffect 的第二个参数,因此您将为每个渲染创建一个新的侦听器。这真的是你想要的吗?从提供给useEffect 的函数中返回一个清理函数也是一个好主意,以便在卸载ConferencingRoom 组件时删除侦听器。 @Tholle 如果我给了一个空数组,状态就会一直为空,不,这不是我想要的 Calling `useVal` several times in a single function with arrays - unexpected behavior的可能重复 @RyanCogswell 我没有在任何函数中多次调用我的 setParticpants,并且问题与 setParticipants 无关,每次调用套接字事件回调时都会读取参与者状态,回调将触发如您在屏幕截图中看到的那样多次,并且每两次状态都会更改为空而不触摸它。 除了不使用功能更新语法(参见我提到的副本)之外,您还需要在每次重新渲染时设置套接字处理程序,而无需清理前一个处理程序。 【参考方案1】:

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

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

我可以通过三种主要方法来处理这个问题:

设置单个套接字事件处理程序,并且仅将 functional updates 用于 participants 状态。使用这种方法,您将为useEffect 使用一个空的依赖数组,并且您不会在效果中引用participants anywhere(包括您的消息处理程序调用的所有方法)。如果您确实引用了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,以便清楚地显示使用participantsparticipantsRef 之间的区别。观察控制台并单击两个按钮,查看将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>
  );

【讨论】:

还有没有更好的方法来做到这一点?不断打开和关闭连接非常疯狂 @Jessica 我意识到您的评论是很久以前的事了,但是如果您仍在寻找另一种选择,我已经用我已经使用的第三种方法(使用参考)更新了这个答案用于由于我希望效果执行的时间而在依赖数组中不想要的依赖的效果。 @Ryan 超级有用的解释,为什么在 useEffects 中调用 setParticipants(或任何 setState 函数)会设置当前(而不是词法范围的变量)? @Ryan 这是一个关于如何使用钩子解决现实世界问题的精彩示例。非常感谢! @RyanCogswell 嗨,希望你一切都好。你能看看这个问题吗?非常感谢您的帮助。谢谢***.com/questions/65678389/…

以上是关于带有 socket.io 状态的 UseEffect 钩子在套接字处理程序中不持久的主要内容,如果未能解决你的问题,请参考以下文章

带有快递的socket.io

带有express和socket.io的节点js-找不到socket.io.js

带有 nginx 的 Socket.io

ExpressJS - 带有路由分离的 Socket.IO

带有 iOS 的 Socket.io 未在移动客户端上连接

带有快速生成器的 socket.io