Need help preventing all other Friend components re-render when one friend is being updated

I have done some improvements by wrapping all children’s props with memo and managed to reduce the re-rendering of all other Friends when a message is being received on the receiver side, and surprisingly the final update after memoizing the props which did the magic was adding useCallback to handleFriendClick, although handleFriendClick is not even needed at that point, yet it still removed the unnecessary re-renders somehow.

Now when I receive a message, I update that friend on the friendslist to display an unread messages count and the other friends do not re-render. However, when I use that handleFriendClick function which made this work after I wrapped it in a useCallback -> it opens the friends message box and this is where all other friends are still being re-rendered -> on handleFriendClick to open, and onClose.

This is the main logic in 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>
  );
};

This are the memoized FriendsList and Friend components

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

The MessageBoxList and MessageBox components are memoized in the same manner. Can you help prevent re-rendering all friends when one friend is being clicked on the friends list and then closed? Also, need advice if my overall approach is generally recommended because I am not fully aware of what I am doing and how to approach this.

1

when I use that handleFriendClick function […] all other friends are still being re-rendered

The immediate cause is that handleFriendClick changes the value of selectedFriends, which is in its dependency array. Hence the useCallback hook returns a new function.

A simple “immediate” solution consists in such cases in leveraging the setSelectedFriends state setter updater function mode (which you already use somehow) to access that selectedFriends state as the previous value, instead of reading it directly.

We can move a lot of logic inside that updater function.

That way, we no longer need to have it in the dependency array:

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!
);

That being said, unless you actually see a performance issue, there is usually no harm having many Components “re-render” (what you observe when console.log in the body of the Components is executed).

This is a normal React behavior, as it re-evaluates its Virtual DOM. But it does not trigger an actual DOM “re-render” (layout and painting, which are the usual performance bottlenecks of browsers), as long as the JSX output is the same.

This should be your case: even if the handleFriendClick callback function changes, it is not used to generate some JSX content, but is (indirectly) attached only as an event listener.

React actually manages all those event listeners globally through event delegation, and does not bother “re-rendering” the actual DOM when only these listeners change.

Sometimes, it may happen that even the React re-evaluation of its Virtual DOM becomes heavy (typically when the Components still perform a lot of computations, without using useMemo). If for some reasons useMemo is still not enough, then indeed we may need React.memo to further avoid React re-renders.

0

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật