React setState перезаписывает весь объект вместо слияния

#javascript #reactjs #web-frontend #react-state

#javascript #reactjs #веб-интерфейс #реагировать-состояние

Вопрос:

Мое состояние выглядит так в конструкторе:

 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}})
  
    }
  

Я получаю сообщение об ошибке, что «push» не существует в stateCopy.timer.timingArray. Я провел некоторое расследование и обнаружил, что this.setState({timer:{screenshotStart:secondsCopy}}); фактически перезаписывает весь объект «timer» в состоянии и удаляет все предыдущие свойства вместо их объединения.

Я не понимаю, что я делаю не так.. Я использую stateCopy, чтобы избежать изменения состояния и получить правильные значения (избегая асинхронной путаницы). Каждая статья, которую я читал онлайн о react, предполагает, что запись объекта в состояние будет объединяться с тем, что уже есть, так почему же он продолжает перезаписывать «таймер» вместо слияния??

Комментарии:

1. @wyfy — Это не будет ничем отличаться от Object.assign . Cmaxster — Остерегайтесь, что они оба являются мелкими копиями.

2. Что такое мелкая копия? Извините, я вроде как новичок..

3. Допустим, у вас есть const obj = {a: 1, b: { c: 1}}; объект со свойством с числовым значением ( a: 1 ) и свойство, значение которого является ссылкой на объект ( b: {c: 1} ). Object.assign (и расширенная нотация) копируйте только верхний уровень, поэтому const obj2 = Object.assign({}, obj); будет make obj.b и obj2.b указывать на тот же объект .

Ответ №1:

Я провел некоторое расследование и обнаружил, что this.setState({timer:{screenshotStart:secondsCopy}}); фактически перезаписывает весь объект «timer» в состоянии и удаляет все предыдущие свойства вместо их объединения.

Правильно. 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.

Ответ №2:

Хуки 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

})
  

Ответ №3:

Ваше 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, b) => a   b, 0) / newTimingArray.length,
            },
        });
    }
}
  

Если captureTime всегда вызывается с помощью true или false , вы можете сделать вещи немного чище с помощью:

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