React聊天应用优化:点击/关闭好友时避免全局重渲染及方案咨询
问题解决与最佳实践评估
问题现状
已通过memo包裹子组件并为handleFriendClick添加useCallback,解决了接收消息时其他Friend组件重渲染的问题,但点击/关闭好友对话框时所有Friend组件仍会全局重渲染。
核心原因分析
点击好友触发handleFriendClick时,会更新selectedFriends状态,而handleFriendClick的依赖数组包含selectedFriends,导致每次selectedFriends变化,handleFriendClick都会重新创建新的函数实例。由于FriendsList和Friend组件接收的handleFriendClick prop发生变化,即使其他好友的friend数据没有改动,所有Friend组件仍会触发重渲染。
解决方案
1. 稳定handleFriendClick函数,移除不必要的依赖
通过useRef跟踪最新状态+函数式更新,避免将selectedFriends放入useCallback的依赖数组,让handleFriendClick保持稳定:
const Home = () => { const { user, token } = useAuth(); const [selectedFriends, setSelectedFriends] = useState([]); const { messages } = useWebSocket(); const [friendsList, setFriendsList] = useState([]); // 用ref跟踪selectedFriends的最新值 const selectedFriendsRef = useRef(selectedFriends); useEffect(() => { selectedFriendsRef.current = selectedFriends; }, [selectedFriends]); // ... 其他逻辑(latestMessage、processedMessagesRef、handleUpdateFriends) // 优化后的fetchMessagesForFriend,用useCallback稳定 const fetchMessagesForFriend = useCallback(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)) { 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); } }, []); // 优化后的handleFriendClick,依赖数组为空,函数保持稳定 const handleFriendClick = useCallback( async (friend) => { const existingFriend = selectedFriendsRef.current.find((f) => f.id === friend.id); if (existingFriend) { if (existingFriend.isMessageBoxOpen) { return; } else if (existingFriend.hasMessageBoxBeenOpenedOnce) { // 用函数式更新获取最新状态,无需依赖selectedFriends 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; } } await fetchMessagesForFriend(friend); setFriendsList((prev) => prev.map((f) => f.id === friend.id ? {...f, unreadMessages: 0} : f ) ); }, [] ); // ... 返回JSX };
2. 简化Friend组件的回调逻辑
由于handleFriendClick已经稳定,Friend组件内无需额外用useCallback包裹点击回调:
let renderCount = 0; const Friend = memo(({ friend, handleFriendClick }) => { console.log("Friend rendered", renderCount++); const handleClick = () => { try { handleFriendClick(friend); } catch (error) { console.log("Failed to fetch messages:", error); } }; return ( <li onClick={handleClick} 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> ); });
最佳实践评估
现有实现的可取之处
- 正确使用
memo包裹子组件,减少不必要的重渲染 - 使用
useCallback稳定回调函数 - 使用
useRef跟踪已处理消息ID,避免重复处理WebSocket消息 - 更新状态时使用函数式更新,确保获取最新状态
可改进的地方
- 状态拆分:
selectedFriends和friendsList存在数据冗余(如unreadMessages),可将未读消息数单独维护为{ [friendId]: number }格式的状态,避免在两个数组中同步更新。 - WebSocket逻辑优化:
handleUpdateFriends依赖selectedFriends和friendsList,可改为用函数式更新获取最新状态,移除这两个依赖,让函数更稳定。 - 错误处理:
fetchMessagesForFriend的错误处理仅输出控制台,可添加用户可见的错误提示。 - 组件职责拆分:
Home组件承担了过多逻辑(WebSocket处理、消息更新、API请求),可抽离为自定义Hook(如useChatMessages、useFriendList),提升代码复用性和可维护性。
内容的提问来源于stack exchange,提问作者lifecycles
相关产品推荐
相关产品推荐

