React中如何仅重绘数组中属性变更的对象(array.map场景)
我原以为这个问题已有类似提问,但搜索后未找到相关内容。
在React中,我将useState存储的对象数组通过array.map映射为ContentComponent进行展示,希望React仅重绘数组中属性发生变更的对象,但尝试的方法要么重绘所有成员,要么都不重绘。简化后的组件代码如下:
const ContainerComponent = () => { const [things, setThings] = useState<Thing[]>([]); const change = () => { const newThing: Thing = {...things[0], name: "Third"}; things.splice(0, 1); const newThings = [...things, newThing]; setThings(newThings); } useEffect(() => { const initialThings: Thing[] = [ { id: 1, name: "First" }, { id: 2, name: "Second"} ]; setThings(initialThings); }, []); return ( <div> <div>Container</div> <div> {things.map(thing => <ContentComponent key={thing.id} thing={thing}/>)} </div> <button onClick={change}>Change</button> </div> ); }
通过React Developer Tools火焰图发现,第一个ContentComponent因「Props changed」重绘(符合预期),但第二个因「父组件重绘」而重绘。
目前我能做到的最优方案是在ContentComponent中使用export default memo(ContentComponent);来避免执行第二个子组件的代码,但有没有办法让React完全不重绘这个未变更的子组件?
解答
核心原理:React的渲染机制
当父组件状态更新时,默认会触发自身及所有子组件的渲染阶段(生成虚拟DOM),只有在提交阶段才会对比虚拟DOM差异,决定是否更新真实DOM。你看到的第二个子组件“重绘”,其实是进入了渲染阶段,而非更新真实DOM。
memo就是你要的最优解
你当前使用的React.memo正是官方推荐的、让未变更子组件完全跳过渲染阶段的方案:
memo会对组件的props做浅比较,若props未发生变化,直接复用之前的虚拟DOM结果,完全跳过该组件的渲染逻辑执行。- 这已经实现了“让React完全不重绘未变更子组件”的需求——这里的“重绘”若指渲染阶段的话,
memo已经彻底阻止了它;若指真实DOM更新,原本就不会发生,因为虚拟DOM没有变化。
修复状态更新的潜在问题
你的change函数存在违反React状态不可变性的问题:直接调用things.splice(0, 1)修改了原状态数组,可能引发不可预测的bug。正确的数组更新方式应该完全基于原状态生成新数组,比如:
const change = () => { setThings(prevThings => prevThings.map((item, index) => index === 0 ? {...item, name: "Third"} : item ) ); }
这种写法既保证了状态不可变,也让memo的浅比较更准确。
其他替代思路(不推荐)
如果不想用memo,还有两种可选方案,但都不如memo简洁高效:
- 拆分独立组件:将每个子项的渲染逻辑封装到单独的组件中并使用
memo,本质和你当前的做法一致。 - 使用
useReducer:通过useReducer管理数组状态,保证更新的不可变性,但仍需配合memo才能跳过未变更子组件的渲染阶段。
总结:React.memo就是解决该问题的标准最优方案,无需额外复杂操作。
内容的提问来源于stack exchange,提问作者micahg

