网站搜索

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