reactjs - 在 React 中,为什么在由 websocket 消息触发的函数中,我的数组会被认为是空的?
在 React 中,我使用 useState 定义了一个数组变量,叫做 messages。我页面上还有一个组件,它没有任何 DOM,而是监听 websocket 上的事件。当我的组件被 websocket 通知一个事件时,我的 messages 变量的长度总是零——尽管它之前有多个消息。为什么在进入这个函数时数组是空的?
这里有一组最小化的代码可以重现该问题:
WebSocketNotificationHandler.tsx:
import { useEffect } from "react";
import config from "../../../config.json";
import { IChatMessage } from "../../../services/chatService";
interface WebSocketNotificationHandlerProps {
onReceiveMessages: (newMessages: IChatMessage[]) => void;
}
export let client: WebSocket = new WebSocket(
config.webSocketAddress,
config.webSocketProtocol
);
const WebSocketNotificationHandler = (
props: WebSocketNotificationHandlerProps
) => {
useEffect(() => {
if (client) {
client.onmessage = async (ev) => {
handleWebSocketMessage(ev);
}
}
}, []);
const handleWebSocketMessage = async (ev: MessageEvent<string>) => {
props.onReceiveMessages([]); // Simplified, normally we send one or more
}
return (
<></>
);
}
export default WebSocketNotificationHandler;
以及 GroupPage.tsx:
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import chatService, { IChatMessage, ICreateMessageResponse } from '../../../services/chatService';
import ChatMessage from '../../atoms/ChatMessage/ChatMessage';
import ChatStagingArea from '../../organisms/ChatStagingArea/ChatStagingArea';
import WebSocketNotificationHandler from '../../organisms/WebSocketNotificationHandler/WebSocketNotificationHandler';
const GroupPage = () => {
const { groupName } = useParams();
const [messages, setMessages] = useState<IChatMessage[]>([]);
const loadGroup = async () => {
setMessages([]);
await chatService // REST API to get current messages, returns a list >0 elements
.getGroup(groupName)
.then(data => {
setMessages([...data.data.messages]);
});
}
useEffect(() => {
loadGroup();
}, []);
const handleMessageSent = (message: ICreateMessageResponse, text: string) => {
let newMessages = [...messages, { /* Message data for a new message */ }];
setMessages(newMessages);
}
const handleNewMessages = (newMessages: IChatMessage[]) => {
console.log(messages.length); // Always 0 even if I can see multiple???
// Code goes here that adds the newMessages to the messages array
};
return (
<>
{messages && messages.map(m => <ChatMessage key={m.id} author={m.fullName} date={m.authoredDate} message={m.message} />)}
<ChatStagingArea groupName={groupName} onSent={(message, text) => handleMessageSent(message, text)} />
<WebSocketNotificationHandler onReceiveMessages={newMessages => handleNewMessages(newMessages)} />
</>
);
}
export default GroupPage;
答案#1
问题
问题在于 handleNewMessages 回调中对 messages 状态的陈旧闭包,该回调在作为 onReceiveMessages 传递给 WebSocketNotificationHandler 的匿名函数中被调用,而该匿名函数由 handleWebSocketMessage 调用,后者实际上实例化了 client.onmessage 事件处理程序。
最初为空的 messages 状态数组是在事件处理程序和回调被实例化时,从初始渲染周期中被闭包捕获的作用域。
解决方案
如果你只是想要附加已发送的消息以及附加接收到的消息数组,那么你在 GroupPage 中的处理程序确实应该使用函数式状态更新。函数式状态更新将一个回调函数传递给状态更新函数,这个回调函数会接收到当前的状态值,然后你计算并返回下一个状态值。这可以避免你尝试更新的状态值过期闭包的问题。
示例:
const handleMessageSent = (message: ICreateMessageResponse, text: string) => {
setMessages(messages => [...messages, { /* Message data for a new message */ }]);
// or setMessages(messages => messages.concat({ /* Message data for a new message */ }));
};
const handleNewMessages = (newMessages: IChatMessage[]) => {
setMessages(messages => [...messages, ...newMessages]);
// or setMessages(messages => messages.concat(...newMessages));
};
如果你想在控制台记录 messages 状态数组的长度,那么为此使用一个单独的 useEffect 钩子:
useEffect(() => {
console.log(messages.length);
}, [messages]);
如果你真的必须在handleNewMessages中记录messages状态数组的长度,那么请使用React ref缓存当前的messages状态值,这样在组件生命周期的任何时候都可以访问ref的当前值。
示例:
const [messages, setMessages] = useState<IChatMessage[]>([]);
const messagesRef = useRef<IChatMessage[]>(messages);
useEffect(() => {
messagesRef.current = messages;
}, [messages]);
...
const handleNewMessages = (newMessages: IChatMessage[]) => {
console.log(messagesRef.current.length);
setMessages(messages => messages.concat(...newMessages));
};
答案#2
据我理解,当执行 handleWebSocketMessage 函数时,它指向消息的初始状态,即使父组件 GroupPage 中的状态已经改变,该状态仍然是一个空数组。
你可以使用 useCallback 钩子来创建 handleNewMessages 函数的记忆化版本,这将确保该函数始终引用 setMessages 函数的最新版本。
const WebSocketNotificationHandler = (props: WebSocketNotificationHandlerProps) => {
const handleWebSocketMessage = useCallback(async (ev: MessageEvent<string>) => {
props.onReceiveMessages([]); // Simplified, normally we send one or more
}, [props.onReceiveMessages]);
};
同样适用于grouppage组件。
const GroupPage = () => {
const handleNewMessages = useCallback((newMessages: IChatMessage[]) => {
console.log(messages.length);
// Code goes here that adds the newMessages to the messages array
}, [messages]);
};
