Websocket 事件在 React 应用程序中接收旧的 redux 状态 [英] Websocket event receiving old redux state in React app
问题描述
我正在使用 Reactjs 和 Redux 构建聊天应用程序.我有两个名为 ChatHeads 和 ChatBox 的组件,它们同时并排安装.
在 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);
步骤:
- 渲染这两个组件后,我将在 ChatHeads 组件中选择一个用户.
- 使用 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'}}
- 现在,每当 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:
- After both the components are rendered, I am selecting a user in the ChatHeads components.
- 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'
}
}
- 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屋!