React子组件UI不更新?详解State数组更新及解决
2025-01-22 08:21:32
React 子组件 UI 未反映 State 数组更新问题
在 React 应用中,当父组件通过 setState
修改状态数组,并希望子组件的 UI 能够同步更新时,有时会遇到子组件 UI 没有及时刷新的情况。这种现象背后的原因值得探究。以下是一些常见的诱因和对应的解决办法。
状态管理:为何更新无效
当父组件更新状态数组 (dataSource
),并通过 props 将数组元素(例如 exp
)传递给子组件 ExpenseEditableBlock
时,子组件可能无法捕获更新,其 UI 也不会刷新。根本原因通常在于,子组件仅在其 props 中的 data
对象或validFlag
属性本身引用发生变化时,才会重新渲染。即使父组件状态数组的内容发生改变,但如果exp
对象的引用没有变动(即:它依然是同一个内存地址,只是里面的属性值被修改了),那么 React 为了优化性能,并不会触发子组件的重新渲染。此外,如果子组件的 state 初始值仅仅是使用了 prop 对象 data
的浅拷贝(像示例中那样)初始化,后续组件 props 中 data
发生任何属性值上的变化,也不会引起组件重新渲染,因为 state
中存储的 expenseData
指向了和 props.data
不再同一个对象了,也就不会监测到父组件状态改变的影响了。
代码问题点
在父组件的 state 中直接更新对象数组,并且数组中单个对象的属性值被修改时,因为 exp
指针没有发生变化,子组件接收到的 prop 指向的仍旧是同一个对象。子组件中又通过 state 维护了一份来自 props 的浅拷贝,导致 state 更新并不会随着 props 更新。
// Parent component - 更新 dataSource 时直接更新对象内部属性
this.setState({dataSource: itemsParam});
// Child component
constructor(props){
super(props);
this.state = {
expenseData: props.data, // 浅拷贝,这里导致问题
// ...其他 state
}
解决方案一:强制更新 prop 数据到 state
使用生命周期函数 componentDidUpdate
(React >= 16.3) 或 getDerivedStateFromProps
(React >= 16.4) ,比较nextProps和this.props的值, 发现数据已经改变,则强制把最新 props 数据 同步到 state, 触发组件的重新渲染。 这有助于确保组件 state 总是与其 props 中的最新值同步,确保在 props.data
更新后能够同步到 state.expenseData
中。
代码示例 (使用 componentDidUpdate):
import React, { Component } from "react";
import '../../CompCss/ExpenseEditableBlock.css';
import 'react-dropdown/style.css';
import { Utils } from './../Utils';
const utils = Utils();
class ExpenseEditableBlock extends Component {
constructor(props){
super(props);
this.state = {
expenseData: props.data,
categoryArray: [],
_otherVarName_ : null,
checked: false
}
console.log('props ----->',props,'\nProps data:',this.state.expenseData);
utils.expCategories().forEach((item) => {
this.state.categoryArray.push(item.label);
});
}
componentDidUpdate(prevProps) {
if (this.props.data !== prevProps.data) { // 判断data prop 是否发生变化,只针对data变化更新
this.setState({ expenseData: this.props.data });
}
}
render() {
return (
<div className={(this.state.expenseData.ValidFlag === "FAIL")?"exp-block-box-one disabled-item":"exp-block-box-one"} style={this.props.style}>
<div id="exp-item-head-section">
<div className="display-inline-block">
<p>Trx ID: {this.state.expenseData.TrxID}</p>
<p>{this.state.expenseData.ValidFlag}</p> /*NOW THIS WILL REFLECT THE VALUE */
<p>{this.props.validFlag}</p> /* THIS IS STILL REFLECTING THE VALUE */
</div>
</div>
<div>
<div className="exp-date-block">{this.state.expenseData.TrxDate}</div>
<div>
<p>{this.state.expenseData.TrxTitle}</p>
</div>
</div>
<div style={{height: '3px'}}>
{/* <TextField id="standard-basic" label="Standard" variant="standard" /> */}
</div>
</div>
)
}
}
export default ExpenseEditableBlock;
使用 componentDidUpdate
之后,可以解决state和prop不同步,带来的UI渲染不同步问题。 注意这里的判断条件,使用了 !==
, 这是对比对象引用是否改变。 当数组内的对象属性值改变时,对象本身并没有发生改变,其内存地址依旧是相同的。这里只能通过检查对象整体引用是否有改变来实现 state 更新。如果想要根据对象内部属性变化更新 UI ,则要考虑深度比较或者更合理的状态管理方案。
解决方案二:生成新的数据对象
修改父组件状态更新方法,生成新的数据对象。 使用扩展运算符(spread operator) ...
或 Object.assign({}, obj)
等方法创建全新的对象副本,避免子组件读取到相同的对象引用。每次父组件状态更新后,exp
对象均是指向不同内存地址的新对象。这样即使内容相同,也会强制触发子组件渲染。
代码示例:
handleDataChange = (data, expID) => {
const itemsParam = [...this.state.dataSource]; // clone a new array
//update the dataSource from parameter.
for(let index = 0; index < itemsParam.length ; index ++ ){
if(itemsParam[index].TrxID === expID){
itemsParam[index] = {...itemsParam[index], ...data} // 新的对象,触发渲染
break;
}
}
this.setState({dataSource: itemsParam});
}
这里在每次数据改变的时候,不是直接在itemsParam
上面进行修改, 而是重新创建一个对象来达到强制触发子组件更新的效果。
解决方案三: 将子组件变为纯组件
可以将子组件 ExpenseEditableBlock
改写为一个纯组件 (React.PureComponent) 。纯组件会对 props 和 state 进行浅层比较。浅比较在对象属性值改变时不会认为props 或者 state 有变化。只有引用的对象发生变化的时候才会被重新渲染。 使用这种方式,也要求父组件在每次 state 更新的时候,创建新的对象。
代码示例:
import React, { PureComponent } from "react"; // 改成 PureComponent
import '../../CompCss/ExpenseEditableBlock.css';
import 'react-dropdown/style.css';
import { Utils } from './../Utils';
const utils = Utils();
class ExpenseEditableBlock extends PureComponent { // 改成 PureComponent
constructor(props){
super(props);
this.state = {
expenseData: props.data,
categoryArray: [],
_otherVarName_ : null,
checked: false
}
console.log('props ----->',props,'\nProps data:',this.state.expenseData);
utils.expCategories().forEach((item) => {
this.state.categoryArray.push(item.label);
});
}
render() {
// 代码与之前的 ExpenseEditableBlock 相同
return (
<div className={(this.state.expenseData.ValidFlag === "FAIL")?"exp-block-box-one disabled-item":"exp-block-box-one"} style={this.props.style}>
<div id="exp-item-head-section">
<div className="display-inline-block">
<p>Trx ID: {this.state.expenseData.TrxID}</p>
<p>{this.state.expenseData.ValidFlag}</p>
<p>{this.props.validFlag}</p>
</div>
</div>
<div>
<div className="exp-date-block">{this.state.expenseData.TrxDate}</div>
<div>
<p>{this.state.expenseData.TrxTitle}</p>
</div>
</div>
<div style={{height: '3px'}}>
{/* <TextField id="standard-basic" label="Standard" variant="standard" /> */}
</div>
</div>
)
}
}
export default ExpenseEditableBlock;
使用这种方法也需要配合解决方案二:生成新的数据对象 ,强制子组件更新。
状态管理建议
在 React 中,高效的状态管理是开发流畅用户体验的关键。 使用 immutable 的方式去管理状态可以有效地降低出错的可能性,确保 UI 的可靠性,并且利于性能优化。当状态对象中存在复杂的嵌套结构的时候,应避免浅拷贝,尽量选择诸如immer.js 等库,以确保正确更新嵌套的状态。选择符合实际应用需求的状态管理策略是打造可维护、高效的 React 应用的关键。