在 React 中,为什么我的数组在由 websocket 消息触发的函数中被认为是空的?

In React, why is my array thought to be empty in a function triggered by a websocket message?

提问人:Nicholas Hill 提问时间:7/15/2023 最后编辑:Drew ReeseNicholas Hill 更新时间:7/28/2023 访问量:174

问:

在 React 中,我有一个使用 定义的数组变量,称为 。我在页面上还有一个组件,它没有任何 DOM,而是在 websocket 上侦听事件。当我的组件从 websocket 收到事件通知时,我的变量的长度始终为零 - 尽管它之前有多条消息。为什么在输入函数时数组是空的?useStatemessagesmessages

下面是重现该问题的最小代码集:

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;
reactjs typescript react-hooks websocket 闭包

评论

0赞 Mayank Kumar Chaudhari 7/22/2023
对于您的简化版本,您将始终得到 0 leanth,因为您正在对空数组进行硬编码。另外,为什么需要使该函数异步?

答:

0赞 Drew Reese 7/22/2023 #1

问题

问题在于,在匿名函数中调用的回调状态的过时闭包实际上实例化了事件处理程序。messageshandleNewMessagesWebSocketNotificationHandleronReceiveMessageshandleWebSocketMessageclient.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));
};

如果要控制台记录状态数组长度,请使用单独的钩子:messagesuseEffect

useEffect(() => {
  console.log(messages.length);
}, [messages]);

如果你真的必须记录状态数组长度,那么使用 React ref 来缓存当前状态值,这样 ref 的当前值就可以在组件生命周期中随时访问。messageshandleNewMessagesmessages

例:

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));
};
0赞 sid 7/22/2023 #2

根据我的理解,当执行函数时,它指向消息的初始状态,即使父组件中的状态已更改,它也是一个空数组。handleWebSocketMessageGroupPage

您可以使用钩子创建函数的记忆版本,这将确保函数始终引用函数的最新版本。useCallbackhandleNewMessagessetMessages

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]);
};
-3赞 Muhammad Idrees 7/28/2023 #3

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(() => {
    const handleWebSocketMessage = async (ev: MessageEvent<string>) => {
      props.onReceiveMessages([]); // Simplified, normally we send one or more
    };

    if (client) {
      client.addEventListener("message", handleWebSocketMessage);
    }

    return () => {
      client.removeEventListener("message", handleWebSocketMessage);
    };
  }, []);

  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.getGroup(groupName).then(data => {
      setMessages([...data.data.messages]);
    });
  };

  useEffect(() => {
    loadGroup();
  }, []);

  useEffect(() => {
    const handleNewMessages = (newMessages: IChatMessage[]) => {
      setMessages(prevMessages => [...prevMessages, ...newMessages]);
    };

    return () => {
      WebSocketNotificationHandler.client.removeEventListener("message", handleNewMessages);
    };
  }, [messages]);

  const handleMessageSent = (message: ICreateMessageResponse, text: string) => {
    let newMessages = [...messages, { /* Message data for a new message */ }];
    setMessages(newMessages);
  };

  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;

这些更改可确保正确处理 WebSocket 事件,并在新消息到达时正确更新消息状态。还负责清理 WebSocket 侦听器以防止内存泄漏。

评论

1赞 Peter Mortensen 7/30/2023
10 个答案,其中一些很长,平均间隔不到 11 分钟,看起来像典型的 ChatGPT 工作流程。一个“我在 2021 年 9 月的最后一次更新”。