Websocket 事件在 React 应用程序中接收旧的 redux 状态 [英] Websocket event receiving old redux state in React app

查看:33
本文介绍了Websocket 事件在 React 应用程序中接收旧的 redux 状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 Reactjs 和 Redux 构建聊天应用程序.我有两个名为 ChatHeadsChatBox 的组件,它们同时并排安装.

ChatHeads 组件中,可以选择用户(您想与之聊天的对象),并且此选择作为 chatInfo 存储在 Redux 存储中.

ChatHeads 组件:

function ChatHeads(props) {常量{派遣,用户信息,用户身份} = 道具;const [chatHeads, setChatHeads] = useState([]);const handleChatHeadSelect = (chatHead, newChat = false) =>{派遣(chatActions.selectChat({isNewChat: newChat,chatId: chatHead.chat._id,聊天用户:chatHead.user}));};const loadChatHeads = async() =>{const response = await services.getRecentChats(userId, userInfo);setChatHeads(response.chats);};useEffect(() => loadChatHeads(), [userInfo]);返回 (//循环使用 ChatHeads 并渲染每个项目//选择项目时,将调用 handleChatHeadSelect);}导出默认连接((状态) =>{返回 {用户信息:state.userInfo,userId: (state.userInfo && state.userInfo.user && state.userInfo.user._id) ||空值,selectedChat: (state.chatInfo && state.chatInfo.chat && state.chatInfo.chat._id) ||空值};},空值,)(聊天头);

聊天操作和减速器:

const initialState = {isNewChat:假,聊天ID: '',聊天用户:{},};const chatReducer = (state = initialState, action) =>{让新状态;开关(动作.类型){案例操作.CHAT_SELECT:newState = { ...action.payload };休息;默认:新状态 = 状态;休息;}返回新状态;};export const selectChat = (payload) =>({类型:actions.CHAT_SELECT,有效载荷,});

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

聊天框组件:

let socket;功能聊天框(道具){const { chatInfo } = props;const onWSMessageEvent = (事件) =>{console.log('onWSMessageEvent => chatInfo', chatInfo);//处理事件};useEffect(() => {socket = services.establishSocketConnection(userId);socket.addEventListener('message', onWSMessageEvent);返回 () =>{socket.close();};}, []);返回 (//如果选择了ChatId//然后渲染聊天//  别的//    黑屏);}导出默认连接((状态)=> {返回 {聊天信息:state.chatInfo};}, null)(ChatBox);

步骤:

  1. 渲染这两个组件后,我将在 ChatHeads 组件中选择一个用户.
  2. 使用 Redux DevTools,我能够观察到 chatInfo 对象已正确填充.

chatInfo: {isNewChat:假,chatId: '603326f141ee33ee7cac02f4',聊天用户:{_id: '602a9e589abf272613f36925',电子邮件:'user2@mail.com',名字:'用户',姓氏:'2',createdOn: '2021-02-15T16:16:24.100Z',更新时间:'2021-02-15T16:16:24.100Z'}}

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

chatInfo: {isNewChat:假,聊天ID: '',聊天用户:{}}

我在这里错过了什么?请建议...

解决方案

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

const { chatInfo } = props;const onWSMessageEvent = (事件) =>{console.log('onWSMessageEvent => chatInfo', chatInfo);//处理事件};

它记住了在这个声明的时刻(这是初始渲染)chatInfo 是什么.值在 store 内和组件渲染范围内更新的回调无关紧要,重要的是回调范围和 chatInfo 在声明回调时所指的内容.>

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

const { chatInfo } = props;//1.创建ref,设置初始值const chatInfoRef = useRef(chatInfo);//2. 当你的 prop 更新时更新当前的 ref 值useEffect(() => chatInfoRef.current = chatInfo, [chatInfo]);//3. 定义现在可以访问当前 prop 值的回调const onWSMessageEvent = (事件) =>{console.log('onWSMessageEvent => chatInfo', chatInfoRef.current);};

您可以查看此代码和框查看使用 ref 和直接使用 prop 的区别.

您可以参考关于过时 props 的文档useRef 文档一个>


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

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

useEffect(() => {socket = services.establishSocketConnection(userId);socket.addEventListener('message', (message) => handleMessage(message, chatInfo));返回 () =>{socket.close();};}, [聊天信息]);

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

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

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

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

function ChatHeads(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)));}, [用户身份]);返回 () =>socket.close();}返回 (//渲染您当前的聊天并将消息作为道具传递)

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

重点是,如果你需要chatInfo来打开socket,那么每次chatInfo变化时,你可能都要打开一个新的socket,所以这是有道理的将依赖项添加到 useEffect 钩子.如果它仅依赖于 userId,则将其向上移动到获得 userId 的位置并连接到那里的套接字.

I am building a Chat application using Reactjs and Redux. I have 2 components called ChatHeads and ChatBox which get mounted side-by-side at the same time.

In the ChatHeads component, the selection of User (to whom you want to chat with) is possible and this selection is stored in the redux store as chatInfo.

ChatHeads Component:

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

Chat Actions & Reducers:

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

In the ChatBox component, I am establishing a socket connection to the server and based on chatInfo object from the global store & ws events, I perform some operations.

ChatBox Component:

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

Steps:

  1. After both the components are rendered, I am selecting a user in the ChatHeads components.
  2. Using the Redux DevTools, I was able to observe that the chatInfo object has been populated properly.

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'
    }
}

  1. Now, whenever the 'message' event gets triggered in the ChatBox component, my expectation is that the chatInfo property should have the latest values. But, I am always getting the initialState instead of the updated ones.

chatInfo: {
    isNewChat: false,
    chatId: '',
    chatUser: {}
}

What am I missing here? Please suggest...

解决方案

The reason for this behaviour is that when you declare your callback

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

it remembers what chatInfo is right at this moment of declaration (which is the initial render). It doesn't matter to the callback that the value is updated inside the store and inside the component render scope, what matters is the callback scope and what chatInfo is referring to when you declare the callback.

If you want to create a callback that can always read the latest state/props, you can instead keep the chatInfo inside a mutable reference.

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

You can check this codesandbox to see the difference between using ref and using the prop directly.

You can consult the docs about stale props and useRef docs


Broadly speaking, the issue is that you're trying to manage a global subscription (socket connection) inside a much more narrow-scope component.

Another solution without useRef would look like

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

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

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

In this case the message event handler is passed the necessary information through arguments, and the useEffect hook re-runs every time we get a new chatInfo.

However, this probably doesn't align with your goals unless you want to open a separate socket for each chat and close the socket every time you switch to a different chat.

Thus, the "proper" solution would entail moving the socket interaction up in your project. One hint is that you are using userId to open the socket, which means that it's supposed to run once you know your userId, not once the user selects a chat.

To move the interaction up, you could store incoming messages in a redux store and pass the messages to the ChatBox component through props. Or you could create connect to the socket in ChatHeads component and pass the messages down to the ChatBox. Something like

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
   )

Or you could create a reducer and dispatch a chatActions.newMessage event and then the messages get to the current chat using redux.

The main point is that if you need chatInfo to open the socket, then every time chatInfo changes, you might have to open a new socket, so it makes sense to add the dependency to the useEffect hook. If it only depends on userId, then move it up to where you get the userId and connect to the socket there.

这篇关于Websocket 事件在 React 应用程序中接收旧的 redux 状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆