React中useMemo无法阻止组件重渲染的问题排查与解决
问题:React组件不必要重渲染的排查与解决
我在React应用中遇到了Video组件不必要重渲染的问题,相关代码如下:
Chat组件中渲染ChatMessage:
{Object.keys(storedUsers).length !== 0 && chatId && Object.keys(chatMessages).length !== 0 && Object.values(chatMessages).map((messageObj, index) => { return <ChatMessage key={index} messageObj={messageObj} /> })}
ChatMessage组件:
const ChatMessage = ({messageObj}) => { // MEMOIZED VALUE const newMessageObj = useMemo(() => {return messageObj}) if (messageObj.admin === true) return const sentById = newMessageObj.sentBy let sentByUser let isOwn if (sentById === userData.userId) { sentByUser = userData isOwn = true } else { sentByUser = storedUsers && storedUsers[sentById] || newChatData && newChatData.otherUsersData } return ( <div className='d-flex flex-row' style={{marginTop: 50}}> <Video messageObject={newMessageObj} /> </div> ) }
Video组件(已用memo包裹):
import React, { memo, useEffect } from 'react' const Video = memo(({messageObject}) => { useEffect(() => { console.log('RE-RENDERED'); }, []) return ( <video> <source src={messageObject.url} /> </video> ) }) export default Video
补充:获取chatMessages的Selector:
const chatMessages = useSelector((state) => state.messages.messagesData[chatId]) || {}
问题:当Chat组件中无关状态变更时,Video组件仍会重渲染。我已经尝试对传入Video的message对象做memo缓存,但无效,请问如何确保Video仅在挂载或messageObject.url变化时重渲染?是什么原因导致了不必要的重渲染?
原因分析
- useMemo使用无效:你写的
useMemo(() => messageObj)没有指定依赖项,且每次ChatMessage渲染时,messageObj是来自父组件的新引用,这个memo完全起不到缓存作用,newMessageObj和messageObj是同一个引用。 - ChatMessage未被memo包裹:父组件Chat渲染时,所有ChatMessage都会被重新创建,哪怕它们的props没有变化,进而导致子组件Video跟着重渲染。
- useSelector可能返回新对象:如果Redux状态更新时(哪怕是和当前chatId无关的消息变化),
state.messages.messagesData[chatId]返回的是新的对象引用,chatMessages会触发更新,导致map重新生成ChatMessage组件。 - Video的memo浅比较特性:memo默认是浅比较props,只要messageObject的引用变化,哪怕url没变,也会触发重渲染;另外你写的useEffect依赖是空数组,只会在组件挂载时执行,无法用来判断后续的重渲染。
- 使用index作为key:用index作为组件key,当列表顺序变化或元素增删时,会导致组件错误地重渲染或挂载,也不利于性能优化。
解决方案
- 给ChatMessage组件添加memo包裹
让ChatMessage只有在props变化时才重渲染:
const ChatMessage = memo(({messageObj}) => { // 原有代码 })
如果需要更精细的控制,可以自定义比较函数,比如只比较messageObj的关键属性:
const ChatMessage = memo(({messageObj}) => { // 原有代码 }, (prevProps, nextProps) => { const prevMsg = prevProps.messageObj; const nextMsg = nextProps.messageObj; return prevMsg.admin === nextMsg.admin && prevMsg.sentBy === nextMsg.sentBy && prevMsg.url === nextMsg.url; })
- 优化useMemo的使用(或直接移除)
既然已经给ChatMessage加了memo,没必要再在内部用useMemo缓存messageObj,直接使用props传入的messageObj即可;如果一定要用,需要指定正确的依赖项:
const newMessageObj = useMemo(() => messageObj, [messageObj.admin, messageObj.sentBy, messageObj.url])
- 优化useSelector的返回值稳定性
使用Redux的shallowEqual来比较返回的对象,避免无关状态变更导致chatMessages引用变化:
import { useSelector, shallowEqual } from 'react-redux'; const chatMessages = useSelector((state) => state.messages.messagesData[chatId] || {}, shallowEqual)
或者用reselect创建一个记忆化的selector,确保只有当目标数据变化时才返回新引用:
import { createSelector } from '@reduxjs/toolkit'; const selectMessagesData = (state) => state.messages.messagesData; const selectChatId = (state, chatId) => chatId; const selectChatMessages = createSelector( [selectMessagesData, selectChatId], (messagesData, chatId) => messagesData[chatId] || {} ); // 在组件中使用 const chatMessages = useSelector((state) => selectChatMessages(state, chatId))
- 给Video组件自定义memo比较逻辑
让Video只在messageObject.url变化时重渲染:
const Video = memo(({messageObject}) => { useEffect(() => { console.log('RE-RENDERED (url changed or mounted)'); }, [messageObject.url]) // 修改依赖,正确监听url变化 return ( <video> <source src={messageObject.url} /> </video> ) }, (prevProps, nextProps) => { return prevProps.messageObject.url === nextProps.messageObject.url; })
- 替换index为唯一key
用messageObj的唯一标识(比如id)作为key,避免不必要的组件重构:
Object.values(chatMessages).map((messageObj) => { return <ChatMessage key={messageObj.id} messageObj={messageObj} /> })
内容的提问来源于stack exchange,提问作者Coding Ninja123211
相关产品推荐
相关产品推荐

