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. Thank you very much for you help!