返回

React子组件UI不更新?详解State数组更新及解决

javascript

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 应用的关键。