Websocket 事件在 React 应用程序中接收旧的 redux 状态

Posted

技术标签:

【中文标题】Websocket 事件在 React 应用程序中接收旧的 redux 状态【英文标题】:Websocket event receiving old redux state in React app 【发布时间】:2021-02-22 05:04:10 【问题描述】:

我正在使用 Reactjs 和 Redux 构建一个聊天应用程序。我有 2 个组件,分别称为 ChatHeadsChatBox,它们同时并排安装。

ChatHeads 组件中,可以选择 User(您要与之聊天的人),并且该选择以 chatInfo 的形式存储在 redux 存储中。

ChatHeads 组件:

function ChatHeads(props) 
  const 
    dispatch,
    userInfo,
    userId
   = props;
  const [chatHeads, setChatHeads] = useState([]);

  const handleChatHeadSelect = (chatHead, newChat = false) => 
    dispatch(
      chatActions.selectChat(
        isNewChat: newChat,
        chatId: chatHead.chat._id,
        chatUser: chatHead.user
      )
    );
  ;

  const loadChatHeads = async () => 
    const response = await services.getRecentChats(userId, userInfo);
    setChatHeads(response.chats);
  ;

  useEffect(() => loadChatHeads(), [userInfo]);

  return (
    //   LOOPING THOUGH ChatHeads AND RENDERING EACH ITEM
    //   ON SELECT OF AN ITEM, handleChatHeadSelect WILL BE CALLED
  );


export default connect(
  (state) => 
    return 
      userInfo: state.userInfo,
      userId: (state.userInfo && state.userInfo.user && state.userInfo.user._id) || null,
      selectedChat: (state.chatInfo && state.chatInfo.chat && state.chatInfo.chat._id) || null
    ;
  ,
  null,
)(ChatHeads);

聊天操作和缩减程序:

const initialState = 
  isNewChat: false,
  chatId: '',
  chatUser: ,
;

const chatReducer = (state = initialState, action) => 
  let newState;
  switch (action.type) 
    case actions.CHAT_SELECT:
      newState =  ...action.payload ;
      break;

    default:
      newState = state;
      break;
  
  return newState;
;

export const selectChat = (payload) => (
  type: actions.CHAT_SELECT,
  payload,
);

ChatBox 组件中,我正在建立与服务器的套接字连接,并基于来自全局存储和 ws 事件的chatInfo 对象,执行一些操作。

聊天框组件

let socket;

function ChatBox(props) 
  const  chatInfo  = props;

  const onWSMessageEvent = (event) => 
    console.log('onWSMessageEvent => chatInfo', chatInfo);
    // handling event
  ;

  useEffect(() => 
    socket = services.establishSocketConnection(userId);

    socket.addEventListener('message', onWSMessageEvent);

    return () => 
      socket.close();
    ;
  , []);

  return (
    //   IF selectedChatId
    //     THEN RENDER CHAT
    //   ELSE
    //     BLANK SCREEN
  );


export default connect((state) => 
  return 
    chatInfo: state.chatInfo
  ;
, null)(ChatBox);

步骤

    两个组件都呈现后,我在 ChatHeads 组件中选择一个用户。 使用 Redux DevTools,我能够观察到 chatInfo 对象已正确填充。
chatInfo: 
    isNewChat: false,
    chatId: '603326f141ee33ee7cac02f4',
    chatUser: 
        _id: '602a9e589abf272613f36925',
        email: 'user2@mail.com',
        firstName: 'user',
        lastName: '2',
        createdOn: '2021-02-15T16:16:24.100Z',
        updatedOn: '2021-02-15T16:16:24.100Z'
    

    现在,每当 ChatBox 组件中触发 'message' 事件时,我的期望是 chatInfo 属性应该具有最新值。但是,我总是得到 initialState 而不是更新的。
chatInfo: 
    isNewChat: false,
    chatId: '',
    chatUser: 

我在这里缺少什么?请建议...

【问题讨论】:

嘿兄弟..请查看ChatBox Component,您在onWSMessageEvent 中记录userInfo,这可能总是相同.. 我认为您应该从event 获取数据 我将chatInfo 记录在onWSMessageEvent 函数中。每当有 ws 事件时,我都会尝试使用它(我从 Redux 商店收到)。这里的问题是我没有得到更新的数据。 你有没有直接尝试return ... action.payload 而不是在case actions.CHAT_SELECT:中分配给newState?? 这没有帮助!正如我所提到的,使用 Redux DevTools,我确实看到商店使用正确的数据进行了更新。但是,在事件中,我仍然得到旧数据...... 【参考方案1】:

这种行为的原因是当你声明你的回调时

const  chatInfo  = props;
const onWSMessageEvent = (event) => 
  console.log('onWSMessageEvent => chatInfo', chatInfo);
  // handling event
;

它记得chatInfo 在声明的这一刻是正确的(这是初始渲染)。值在 store 内和组件渲染范围内更新与回调无关,重要的是回调范围以及声明回调时chatInfo 所指的内容。

如果您想创建一个始终可以读取最新状态/道具的回调,您可以将chatInfo 保留在可变引用中。

const  chatInfo  = props;
// 1. create the ref, set the initial value
const chatInfoRef = useRef(chatInfo);
// 2. update the current ref value when your prop is updated
useEffect(() => chatInfoRef.current = chatInfo, [chatInfo]);
// 3. define your callback that can now access the current prop value
const onWSMessageEvent = (event) => 
  console.log('onWSMessageEvent => chatInfo', chatInfoRef.current);
;

你可以查看this codesandbox,看看使用ref和直接使用prop的区别。

您可以咨询docs about stale props和useRef docs


从广义上讲,问题在于您正试图在一个范围更窄的组件内管理全局订阅(套接字连接)。

没有useRef 的另一种解决方案看起来像

useEffect(() => 
    socket = services.establishSocketConnection(userId);

    socket.addEventListener('message', (message) => handleMessage(message, chatInfo));

    return () => 
      socket.close();
    ;
  , [chatInfo]);

在这种情况下,消息事件处理程序通过参数传递必要的信息,并且每次我们得到一个新的chatInfo 时,useEffect 挂钩都会重新运行。

但是,这可能与您的目标不符,除非您想为每个聊天打开一个单独的套接字,并在每次切换到不同的聊天时关闭该套接字。

因此,“适当的”解决方案需要在您的项目中向上移动套接字交互。一个提示是您正在使用 userId 打开套接字,这意味着它应该在您知道您的 userId 后运行,而不是在用户选择聊天时运行。

要向上移动交互,您可以将传入的消息存储在 redux 存储中,并通过 props 将消息传递给 ChatBox 组件。或者您可以在ChatHeads 组件中创建连接到套接字并将消息传递给ChatBox。类似的东西

function ChatHeads(props) 
  const 
    dispatch,
    userInfo,
    userId
   = props;
  const [chatHeads, setChatHeads] = useState([]);

  const loadChatHeads = async () => 
    const response = await services.getRecentChats(userId, userInfo);
    setChatHeads(response.chats);
  ;

  useEffect(() => loadChatHeads(), [userInfo]);

  const [messages, setMessages] = useState([]);
  useEffect(() => 
    socket = services.establishSocketConnection(userId);
    socket.addEventListener('message', (msg) => setMessages(messages.concat(msg)));
  , [userId]);
    return () => socket.close();


   return (
   // render your current chat and pass the messages as props
   )

或者您可以创建一个 reducer 并发送一个 chatActions.newMessage 事件,然后使用 redux 将消息发送到当前聊天。

主要的一点是,如果你需要chatInfo来打开socket,那么每次chatInfo改变时,你可能需要打开一个新的socket,所以将依赖添加到useEffect钩子是有意义的.如果它只依赖于userId,则将其移至您获得userId 的位置并连接到那里的套接字。

【讨论】:

这解决了这个问题。但是,如果我有一些我想在回调中使用的变量,我是否应该以类似的方式为所有这些变量创建引用。有什么优雅的方法可以解决这个问题吗? @SreekarMouli 我添加了一些我认为可以帮助您的提示。 感谢您的解决方案。另一个快速的疑问。在父组件中,我使用useRef 创建了一个socket 变量。我将这个socket 传递给我想收听一些事件的子组件。但是,socket.current 为空。在socket.current 具有 ws 连接后,如何让子组件重新渲染。我不能在子组件的useEffect 中使用socket.current,因为可变对象不能用作依赖项,我猜? 这不是一门精确的科学,很难说,但我会仔细检查为什么有必要让子组件处理套接字。我通常会尝试在一个地方进行套接字交互,然后在套接字和应用程序之间创建一些粘合剂。您的应用似乎是关于聊天的,因此组件可能应该传递聊天、消息、用户等。 换句话说,当您的套接字收到消息时,使用它来更新您的应用程序状态。然后组件使用此状态来呈现 UI 并处理交互。他们不关心状态是否因套接字消息、setTimeout 触发器、API 调用或用户操作而改变。

以上是关于Websocket 事件在 React 应用程序中接收旧的 redux 状态的主要内容,如果未能解决你的问题,请参考以下文章

React-Native Websocket事件数据属性丢失

webSocket.send() 没有在 react js 中执行?

为啥 useState() 会重复 websocket 事件?

websocket在React中多次连接

React js从父事件中更新子状态

使用 React、Redux 和 Websocket 处理请求