ReactJS问题:调用useState setter后父组件未触发重新渲染
父组件FatherComponent通过useState维护一个对象列表objectList,遍历该列表生成子组件ChildComponent。点击父组件的「Add Component」按钮可以添加新子组件,但点击子组件的「Remove Component」按钮时,虽然objectList确实被修改了,却不会触发父组件重新渲染,只有添加按钮能触发重渲染。
相关代码如下:
父组件代码
const FatherComponent = ({data}) => { const [objectList, setObjectList] = useState(data.objects) // 每个objectList元素创建时都会分配递增的唯一id,用于删除对应对象 const removeComponent = (id) => { let newObjectList = objectList let index = newObjectList.findIndex(object => object.id === id); newObjectList.splice(index, 1); setObjectList(newObjectList) } const addComponent = (prevObjects) => { setObjectList([...prevObjects, new Object(data.newObjectId)]) data.newObjectId += 1 } useEffect(() => { data.objects = objectList }, [objectList, data]) return ( <Fragment> ... <button onClick={() => addComponent(objectList)}>Add Component</button> {objectList.map((childComponent) => ( <ChildComponent object={childComponent} removeComponent={() => removeComponent(childComponent.id)} /> ))} </Fragment> ) }
子组件代码
const ChildComponent = ({ object, removeComponent }) => { return ( ... <button onClick={removeComponent}>Remove Component</button> ... ); }
注:希望objectList作为大型类结构的一部分存储工具信息,因此在useEffect中同步更新data.objects;另外误以为每次重渲染时useState都会用data的值初始化,所以必须同步更新。
疑问:调用removeComponent时明明调用了useState的setter,为什么没触发父组件重渲染?
核心原因
React的状态更新依赖浅比较判断是否重渲染:当调用setObjectList时,如果传入的是和原状态同一引用的值,React会认为状态没有变化,从而跳过重渲染。
在removeComponent函数中:
let newObjectList = objectList // 仅复制原数组引用,未创建新数组 newObjectList.splice(index, 1); // 直接修改原数组(引用指向同一内存地址) setObjectList(newObjectList) // 传入的仍是原数组引用,React判定状态未变更
而addComponent中用扩展运算符[...prevObjects, ...]创建了新数组,引用地址不同,React能识别到状态变化并触发重渲染。
此外还有两个潜在问题:
- 直接修改
props.data(如data.objects = objectList和data.newObjectId += 1):React中props是只读的,直接修改会导致状态管理混乱,违反单向数据流原则。 - 误解
useState初始化逻辑:useState(data.objects)仅在组件第一次挂载时使用初始值,后续重渲染不会再用data.objects覆盖状态,无需在useEffect中同步data.objects。
修复方案
1. 修正removeComponent,创建新数组更新状态
推荐两种实现方式:
方式一:用filter创建新数组(简洁高效)
const removeComponent = (id) => { // 过滤掉目标id的对象,返回新数组 const newObjectList = objectList.filter(object => object.id !== id); setObjectList(newObjectList); }
方式二:复制原数组后再修改
const removeComponent = (id) => { // 用扩展运算符复制原数组,创建新数组 const newObjectList = [...objectList]; const index = newObjectList.findIndex(object => object.id === id); if (index !== -1) { // 避免找不到id时出错 newObjectList.splice(index, 1); setObjectList(newObjectList); } }
2. 移除对props.data的直接修改
将newObjectId移到组件内部状态管理,避免修改props:
const FatherComponent = ({data}) => { // 用data.newObjectId作为初始值,维护组件内部的id计数器 const [nextObjectId, setNextObjectId] = useState(data.newObjectId); const [objectList, setObjectList] = useState(data.objects) const addComponent = () => { // 使用内部状态的id创建新对象,之后更新计数器 setObjectList(prev => [...prev, { id: nextObjectId }]); setNextObjectId(prev => prev + 1); } // 移除修改data的useEffect // useEffect(() => { // data.objects = objectList // }, [objectList, data]) return ( <Fragment> <button onClick={addComponent}>Add Component</button> {objectList.map((childComponent) => ( <ChildComponent key={childComponent.id} // 给列表元素加唯一key,避免渲染异常 object={childComponent} removeComponent={() => removeComponent(childComponent.id)} /> ))} </Fragment> ) }
3. 给列表子组件添加key
遍历objectList生成ChildComponent时,必须添加唯一的key属性(用childComponent.id即可),这是React列表渲染的最佳实践,能避免不必要的重渲染和组件状态混乱。
最终效果
修改后,点击子组件的「Remove Component」按钮时,会创建新数组并更新状态,React识别到状态引用变化,会触发父组件重渲染,对应的子组件也会被正确移除。
内容的提问来源于stack exchange,提问作者Rodolfo

