需要帮助防止在更新一位好友时所有其他好友组件重新渲染

问题描述 投票:0回答:1

我通过用备忘录包装所有儿童道具做了一些改进,并设法减少了接收方收到消息时所有其他朋友的重新渲染,令人惊讶的是,在记住发挥魔力的道具后的最终更新是将 useCallback 添加到handleFriendClick,尽管此时甚至不需要handleFriendClick,但它仍然以某种方式删除了不必要的重新渲染。现在,当我收到消息时,我会更新朋友列表上的该朋友以显示未读消息计数,并且其他朋友不会重新呈现。但是,当我使用该handleFriendClick函数时,该函数在我将其包装在useCallback中后使其工作 - >它打开朋友消息框,这是所有其他朋友仍在重新渲染的地方 - >在handleFriendClick上打开,然后onClose。

这是Home.jsx中的主要逻辑

const Home = () => {
  const { user, token } = useAuth();
  const [selectedFriends, setSelectedFriends] = useState([]);
  const { messages } = useWebSocket();
  const [friendsList, setFriendsList] = useState([]);

  // Memoize the latest message to avoid unnecessary updates
  const latestMessage = messages.length > 0 ? messages[messages.length - 1] : null;
  // Track sent/received messageIds to avoid duplicates from re-rendering or re-adding messages
  const processedMessagesRef = useRef(new Set());

  // on new message (websocket)
  const handleUpdateFriends = useCallback(
    (message) => {
      // Check if the message has already been added using processedMessagesRef
      if (processedMessagesRef.current.has(message.messageId)) return;
      // Mark this message as handled to prevent re-adding it
      processedMessagesRef.current.add(message.messageId);
      // sender side
      const isSender = message.senderId === user.id;
      if (isSender) {
        setSelectedFriends((prev) => prev.map((f) => (f.id === message.receiverId ? { ...f, storedMessages: [...f.storedMessages, message] } : f)));
        return;
      }
      // receiver side
      const existingFriend = selectedFriends.find((f) => f.id === message.senderId);
      if (existingFriend) {
        setSelectedFriends((prev) => prev.map((f) => (f.id === message.senderId ? { ...f, storedMessages: [...f.storedMessages, message] } : f)));
        if (!existingFriend.isMessageBoxOpen) {
          setFriendsList((prev) => prev.map((f) => (f.id === message.senderId ? { ...f, unreadMessages: (f.unreadMessages || 0) + 1 } : f)));
        }
      } else {
        console.log("receiver side newFriend");
        const friend = friendsList.find((f) => f.id === message.senderId);
        if (friend) {
          setFriendsList((prev) => prev.map((f) => (f.id === message.senderId ? { ...f, storedMessages: [...(f.storedMessages || []), message], unreadMessages: (f.unreadMessages || 0) + 1 } : f)));
        }
      }
    },
    [selectedFriends, friendsList, user.id]
  );

  // on new message (websocket)
  useEffect(() => {
    if (!latestMessage) return;
    handleUpdateFriends(latestMessage);
  }, [latestMessage, handleUpdateFriends]);

  const fetchMessagesForFriend = async (friend) => {
    try {
      const response = await axios.get(`http://localhost:8080/api/chat/messages/${friend.friendshipId}`, {
        params: {
          limit: 100,
        },
      });
      if (response.status === 204) {
        console.log("No messages found.");
      } else if (Array.isArray(response.data)) {
        console.log("response.data", response.data);
        const friendWithMessages = { ...friend, storedMessages: response.data.reverse(), isMessageBoxOpen: true, hasMessageBoxBeenOpenedOnce: true };
        setSelectedFriends((prev) => {
          if (prev.length >= 2) {
            return [prev[1], friendWithMessages];
          }
          return [...prev, friendWithMessages];
        });
      }
    } catch (error) {
      console.error("Failed to fetch messages:", error);
    }
  };

  // on friend click
  const handleFriendClick = useCallback(
    async (friend) => {
      console.log("friend", friend);
      const existingFriend = selectedFriends.find((f) => f.id === friend.id);
      if (existingFriend) {
        if (existingFriend.isMessageBoxOpen) {
          // Case 1: Message box is already open, no need to change anything
          return;
        } else if (existingFriend.hasMessageBoxBeenOpenedOnce) {
          // Case 2: Message box has been opened before but is currently closed,
          // reopens the message box without fetching messages and resets unread messages
          setSelectedFriends((prev) => prev.map((f) => (f.id === friend.id ? { ...f, isMessageBoxOpen: true, unreadMessages: 0 } : f)));
          setFriendsList((prev) => prev.map((f) => (f.id === friend.id ? { ...f, unreadMessages: 0 } : f)));
          return;
        }
      }
      // Case 3: Message box has never been opened before, fetch messages and open the message box by adding a new friend with isMessageBoxOpen: true
      await fetchMessagesForFriend(friend);
      // reset unread messages
      setFriendsList((prev) => prev.map((f) => (f.id === friend.id ? { ...f, unreadMessages: 0 } : f)));
    },
    [selectedFriends]
  );

  return (
    <div>
      {" "}
      <FriendsList friendsList={friendsList} friendsListLoading={friendsListLoading} friendsListError={friendsListError} handleFriendClick={handleFriendClick} /> <MessageBoxList selectedFriends={selectedFriends} setSelectedFriends={setSelectedFriends} />
    </div>
  );
};

这是记忆的 FriendsList 和 Friend 组件

const FriendsList = memo(({ friendsList, friendsListLoading, friendsListError, handleFriendClick }) => {
  return (
    <aside className="w-[250px] bg-white border-l border-gray-300 p-4">
      <h2 className="text-lg font-semibold mb-4"> Friends </h2> {friendsListError && <p className="text-red-500"> {friendsListError} </p>} {friendsListLoading && <p> Loading... </p>}
      <ul> {friendsList.length > 0 ? friendsList.map((friend) => <Friend key={friend.id} friend={friend} handleFriendClick={handleFriendClick} />) : <li className="py-2 text-gray-500">No friends found</li>} </ul>{" "}
    </aside>
  );
});
let renderCount = 0;

const Friend = memo(({ friend, handleFriendClick }) => {
  console.log("Friend rendered", renderCount++);
  console.log("friend username", friend.username, friend);
  const onHandleFriendClick = useCallback(
    async (friend) => {
      try {
        // call the parent function (Home->FriendList->Friend passed through props) to update the messages state on "Home"
        handleFriendClick(friend);
      } catch (error) {
        console.log("Failed to fetch messages:", error);
      }
    },
    [handleFriendClick]
  );

  return (
    <li onClick={() => onHandleFriendClick(friend)} key={friend.id} className="flex py-2 border-b border-gray-200 cursor-pointer hover:bg-gray-200 rounded-md">
      <div className="px-2"> {friend.username.length > 20 ? friend.username.slice(0, 20) + "..." : friend.username} </div> {friend.unreadMessages > 0 && <div className="bg-red-500 text-white rounded-full px-2 ml-2"> {friend.unreadMessages} </div>}{" "}
    </li>
  );
});

MessageBoxList 和 MessageBox 组件以相同的方式被记忆。当在朋友列表中点击一位朋友然后关闭时,您能否帮助防止重新渲染所有朋友?另外,如果我的总体方法得到普遍推荐,则需要建议,因为我不完全了解我在做什么以及如何解决这个问题。

javascript reactjs react-memo
1个回答
1
投票

当我使用该handleFriendClick 函数时[...]所有其他好友仍在重新渲染

直接原因是

handleFriendClick
更改了
selectedFriends
的值,该值位于其依赖数组中。因此
useCallback
钩子返回一个新函数。

在这种情况下,一个简单的“立即”解决方案包括利用

setSelectedFriends
状态设置器 更新器函数 模式(您已经以某种方式使用)来访问
selectedFriends
状态作为先前的值,而不是直接读取它。

我们可以在更新器函数中移动很多逻辑。

这样,我们就不再需要将它放在依赖数组中:

const handleFriendClick = useCallback( async (friend) => { // Use a flag to know if more action is needed let needFetch = false; // Leverage the state setter updater function mode // to get access to the state (previous) value, // instead of reading it directly setSelectedFriends((previousSelectedFriends) => { // Some logic can be moved inside the updater function const existingFriend = previousSelectedFriends.find((f) => f.id === friend.id); if (existingFriend) { if (existingFriend.isMessageBoxOpen) { // Case 1: Message box is already open, no need to change anything // Now make sure to return the same state value // for the updater function return previousSelectedFriends; } else if (existingFriend.hasMessageBoxBeenOpenedOnce) { // Case 2: Message box has been opened before but is currently closed, // reopens the message box without fetching messages and resets unread messages setFriendsList((prev) => prev.map((f) => (f.id === friend.id ? { ...f, unreadMessages: 0 } : f))); // Now return the ne state value // for the updater function return previousSelectedFriends.map((f) => (f.id === friend.id ? { ...f, isMessageBoxOpen: true, unreadMessages: 0 } : f)); } } // Case 3: Message box has never been opened before, fetch messages and open the message box by adding a new friend with isMessageBoxOpen: true needFetch = true; // Now make sure to return the same state value // for the updater function return previousSelectedFriends; }); // end of setSelectedFriends // More actions to perform, // which do not need the selectedFriends value if (needFetch) { await fetchMessagesForFriend(friend); // reset unread messages setFriendsList((prev) => prev.map((f) => (f.id === friend.id ? { ...f, unreadMessages: 0 } : f))); } }, [] // With this, the useCallback no longer depends on selectedFriends state directly! );


话虽这么说,除非您确实看到性能问题,否则让许多组件“重新渲染”通常不会有什么坏处(当执行组件主体中的

console.log

 时您观察到的情况)。

这是正常的 React 行为,因为它会重新评估其 Virtual DOM。但只要 JSX 输出相同,它就不会触发实际的 DOM“重新渲染”(布局和绘制,这是浏览器通常的性能瓶颈)。

这应该是你的情况:即使

handleFriendClick

回调函数发生变化,它也不会用于
生成一些JSX内容,而是(间接)仅作为事件侦听器附加。

React 实际上通过事件委托全局管理所有这些事件监听器,并且当只有这些监听器发生变化时,不会费心“重新渲染”实际 DOM。

有时,即使 React 重新评估其 Virtual DOM 也可能变得繁重(通常当组件仍然执行大量计算,而不使用

useMemo

 时)。如果由于某些原因 
useMemo
 仍然不够,那么我们确实可能需要 
React.memo
 来进一步避免 React 重新渲染。

© www.soinside.com 2019 - 2024. All rights reserved.