#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);
будет makeobj.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