我通过用备忘录包装所有儿童道具做了一些改进,并设法减少了接收方收到消息时所有其他朋友的重新渲染,令人惊讶的是,记住发挥魔力的道具后的最终更新是将 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 组件以相同的方式被记忆。当在朋友列表中点击一位朋友然后关闭时,您能否帮助防止重新渲染所有朋友?另外,如果我的总体方法得到普遍推荐,则需要建议,因为我不完全了解我在做什么以及如何处理这个问题。非常感谢您的帮助!
当我使用该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!
);