反应SetState覆盖整个对象而不是合并

问题描述

我的状态在构造函数中如下所示:

this.state = {
      selectedFile: null,//current file selected for upload.
      appStatus: 'waiting for zip...',//status view
      zipUploaded: false,zipUnpacked: false,capturingScreens: false,finishedCapture: false,htmlFiles: null,generatedList: [],optionValues: { 
        delayValue: 1
      },sessionId: null,estimatedTime: null,zippedBackupFile: null,secondsElapsed:0,timer: {
        screenshotStart:0,screenshotEnd:0,timingArray:[],averageTimePerUnit:25,totalEstimate:0
      }
    };

我的app.js中具有以下功能

this.secondsCounter = setInterval(this.countSeconds,1000); // set inside the constructor


getStatecopy = () => Object.assign({},this.state);

countSeconds = () => {
    let statecopy = this.getStatecopy();
   
    let currentSeconds = statecopy.secondsElapsed + 1;

    this.setState({secondsElapsed:currentSeconds});
  }
  captureTime = (startOrStop) => {
    let statecopy = this.getStatecopy();
    let secondscopy = statecopy.secondsElapsed;
    let startPoint;

    if(startOrStop === true) {
      this.setState({timer:{screenshotStart:secondscopy}});
    } else if(startOrStop === false){
      this.setState({timer:{screenshotEnd:secondscopy}});
      startPoint = statecopy.timer.screenshotStart;
      statecopy.timer.timingArray.push((secondscopy-startPoint));
      this.setState({secondsElapsed:secondscopy})
      
      statecopy.timer.averageTimePerUnit = statecopy.timer.timingArray.reduce((a,b) => a + b,0) / statecopy.timer.timingArray.length;
      this.setState({secondsElapsed:secondscopy})
      this.setState({timer:{averageTimePerUnit:statecopy.timer.averageTimePerUnit}})
  
    }

我收到一个错误,指出statecopy.timer.timingArray上不存在“ push”。我进行了一些调查,发现this.setState({timer:{screenshotStart:secondscopy}});实际上是覆盖状态中的整个“计时器”对象,并删除所有先前的属性,而不是合并它们。

我不明白我在做什么错..我正在使用statecopy来避免对状态进行变异,并获取适当的值(避免异步混淆)。我在网上阅读的每一篇关于react的文章都表明,编写一个要声明状态的对象将与已经存在的对象合并,所以为什么它会不断覆盖“ timer”而不是合并?

解决方法

我做了一些调查,发现this.setState({timer:{screenshotStart:secondsCopy}});实际上是覆盖状态中的整个“计时器”对象,并删除所有先前的属性,而不是合并它们。

正确。 setState仅在顶级处进行合并。低于此的任何东西都必须处理。例如:

this.setState(({timer}) => {timer: {...timer,screenshotStart: secondsCopy}});

请注意使用setState的回调版本。重要的是,必须在任何时间内提供依赖于现有状态的状态信息。

在其他地方,您也必须执行相同的操作,包括当您推送到数组时。以下是一些其他说明:

没有理由在此处复制状态:

countSeconds = () => {
    let stateCopy = this.getStateCopy();
    let currentSeconds = stateCopy.secondsElapsed + 1;
    this.setState({secondsElapsed: currentSeconds});
}

...并且(如上所述),您必须使用回调形式来基于现有状态可靠地修改状态。相反:

countSeconds = () => {
    this.setState(({secondsElapsed}) => {secondsElapsed: secondsElapsed + 1});
};

类似地在captureTime中:

captureTime = (startOrStop) => {
    if (startOrStop) { // *** There's no reason for `=== true`
        this.setState(({timer,secondsElapsed}) => {timer: {...timer,screenshotStart: secondsElapsed}});
    } else { // *** Unless `startOrStop` may be missing or something,no need for `if` or `=== false`.
        this.setState(({timer,secondsElapsed}) => {
            const timingArray = [...timer.timingArray,secondsElapsed - timer.screenshotStart];
            const update = {
                timer: {
                    ...timer,screenshotEnd: secondsElapsed,timingArray,averageTimePerUnit: timingArray.reduce((a,b) => a + b,0)
                }
            };
        });
    }
};

旁注:您的copyState函数执行状态复制。因此,如果您修改其包含的对象的任何属性,那么您将直接修改状态,而在React中则不必这样做。

,

setState挂钩总是用一个新对象覆盖状态……这是它们的正确行为。

您需要在setState中使用一个函数。不只是传递一个对象。

setState((prevState,prevProps)=>{
//logic to make a new object that you will return ... copy properties from prevState as needed.
 //something like const newState = {...prevState} //iffy myself on exact syntax

return newState

})
,

您的getStateCopy仅克隆现有状态-嵌套的任何内容都不会克隆。为了说明:

const getStateCopy = () => Object.assign({},state);
const state = {
  foo: 'bar',arr: [1,2]
};

const shallowCopy = getStateCopy();
shallowCopy.foo = 'newFoo';
shallowCopy.arr.push(3);
console.log(state);

或者先深度克隆状态,或者使用spread添加所需的新属性:

countSeconds = () => {
    this.setState({
        ...this.state,secondsElapsed: this.state.secondsElapsed + 1
    });
}
captureTime = (startOrStop) => {
    if (startOrStop === true) {
        this.setState({ ...this.state,timer: { ...this.timer,screenshotStart: this.state.secondsElapsed } });
    } else if (startOrStop === false) {
        const newTimingValue = this.state.secondsElapsed - this.state.timer.screenshotStart;
        const newTimingArray = [...this.state.timer.timingArray,newTimingValue];
        this.setState({
            ...this.state,timer: {
                ...this.timer,screenshotEnd: this.state.secondsElapsed,timingArray: newTimingArray,averageTimePerUnit: newTimingArray.reduce((a,0) / newTimingArray.length,},});
    }
}

如果始终使用captureTimetrue调用false,则可以使用以下方法使事情看起来更简洁:

captureTime = (startOrStop) => {
    if (startOrStop) {
        this.setState({ ...this.state,screenshotStart: this.state.secondsElapsed } });
        return;
    }
    const newTimingValue = this.state.secondsElapsed - this.state.timer.screenshotStart;
    const newTimingArray = [...this.state.timer.timingArray,newTimingValue];
    // etc