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

问题描述 投票: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-hooks memo
1个回答
0
投票

当我使用该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 update function mode
    // to get access to the state (previous) value,
    // instead of reading it directly
    setSelectedFriends((previousSelectedFriends) => {
      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 update 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 update 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 update function
      return previousSelectedFriends;
    });

    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!
);
© www.soinside.com 2019 - 2024. All rights reserved.