Prevent Do not mutate state directly. Use setState() - javascript

I have a function to add item on bag. It's working pretty cool but I got a warning Do not mutate state directly. Use setState();
How can I use this.setState() instead use this.state.bagList[item.id] = {...item};
addToBag = (item) => {
let itemFound = false;
Object.keys(this.state.bagList).map((key) => {
const bagItem = this.state.bagList[key];
if(bagItem.id === item.id) {
itemFound = true;
bagItem.quantity++;
return bagItem;
} else {
return bagItem;
}
});
if(!itemFound) {
this.state.bagList[item.id] = {...item};
}
const newbagList = this.state.bagList;
this.setState({ bagList: newbagList });
localStorage.setItem("productsOnBag", JSON.stringify(newbagList));
this.showBag();
}
I expect prevent this warning and mutate the state correctly.

You can use setState in the following way to update bagList without the mutation.
this.setState((prevState) => ({
bagList: {
...prevState.bagList,
[item.id]: {...item}
}
}))
Hope this will help!

I tried some other approaches, but I solved the warnings just adding my state in a const and attributing the {...item}. Here is the code:
...
if(!itemFound) {
const addItemToBagList = this.state.bagList;
addItemToBagList[item.id] = {...item };
}

The problem is in the order of your code.
You are modifying state directly here:
if(!itemFound) {
this.state.bagList[item.id] = {...item};
}
That's why it's throwing an error.
You should first create newbagList, and after that modify it and this.setState({ bagList: newbagList });

Related

React, conditional rendering wont register change in object

The if statement in canBookSlot() is only checked once for some reason. The second time canBookSlot() is triggered, the userDetailsObj.canBook should be 0 after running updateUser(). And according to the console log it is the case, but the if statement still runs, why?
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const updateUser = () => {
userDetailsObj["hasBooked"] = 1;
userDetailsObj["canBook"] = 0;
};
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
After each render userDetailsObj will take that value from localStorage. That's how every variable inside a component which isn't a state made with useState hook, or a ref made with useRef hook behaves. You can fix your problem this by using a state, like so:
const [userDetails, setUserDetails] = useState(JSON.parse(localStorage.getItem("userDetails")));
const updateUser = () => {
const newUserDetails = { ...userDetailsObj, hasBooked: 1, canBook: 0 };
setUserDetails(newUserDetails);
localStorage.setItem("userDetails", JSON.stringify(newUserDetails));
};
const canBookSlot = (id) => {
if (userDetails.canBook != 0) {
//Always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
Can you clarify where this code runs? Are you using a class component or functional component? Would you mind sharing the entire component? If it is doing what I think it is doing, the let userDetailsString = localStorage.getItem("userDetails"); is running every render which means on every render, it grabs the value in localStorage and uses that, rather than using your object stored in userDetailsObj.
If you are using functional components, you could fix this by using state.
let userDetailsString = localStorage.getItem("userDetails");
let [userDetailsObj, updateUserDetailObj] = useState(JSON.parse(userDetailsString));
const updateUser = () => {
let u = { ...userDetailsObj,
hasBooked: 1,
canBook: 0,
}
updateUserDetailObj(u);
};
If you are using class Components, let me know and I'll update it with that option.

React SetState Overwriting Entire Object Instead of Merging

My state looks like this in the constructor:
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
}
};
I have the following functions in my 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}})
}
I'm getting an error that "push" does not exist on stateCopy.timer.timingArray. I did some investigation and found that this.setState({timer:{screenshotStart:secondsCopy}}); is actually overwriting the entire "timer" object in state and removing all of the previous properties instead of merging them.
I don't understand what I'm doing wrong.. I'm using stateCopy to avoid mutating state, and to get proper values (avoiding asynchronous confusion). Every article I read online about react suggests that writing an object to state will merge with whatever is already there, so why does it keep overwriting "timer" instead of merging??
I did some investigation and found that this.setState({timer:{screenshotStart:secondsCopy}}); is actually overwriting the entire "timer" object in state and removing all of the previous properties instead of merging them.
Correct. setState only handles merging at the top level. Anything below that you have to handle yourself. For instance:
this.setState(({timer}) => {timer: {...timer, screenshotStart: secondsCopy}});
Note the use of the callback version of setState. It's important to do that any time you're providing state information that's dependent on existing state.
There are other places you have to do the same sort of thing, including when you push to the array. Here are some further notes:
There's no reason to copy state here:
countSeconds = () => {
let stateCopy = this.getStateCopy();
let currentSeconds = stateCopy.secondsElapsed + 1;
this.setState({secondsElapsed: currentSeconds});
}
...and (as I mentioned above) you must use the callback form to reliably modify state based on existing state. Instead:
countSeconds = () => {
this.setState(({secondsElapsed}) => {secondsElapsed: secondsElapsed + 1});
};
Similarly in 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)
}
};
});
}
};
Side note: Your copyState function does a shallow state copy. So if you modify any properties on the objects it contains, you'll be directly modifying state, which you mustn't do in React.
setState hooks overwrite the state with a new object always... that is their correct behavior.
you need to use a function within setState. not just pass in an object.
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
})
Your getStateCopy is only shallow cloning the existing state - anything nested is not cloned. To illustrate:
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);
Either deep clone the state first instead, or use spread to add in the new properties you want:
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,
},
});
}
}
If captureTime is always called with either true or false, you can make things look a bit cleaner with:
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

How to correctly update state in array using react?

In a previous question, I was given an answer on how to update an array, which was achieved in the following way:
onClick(obj, index) {
if (data.chosenBets[index]) {
// Remove object.
data.chosenBets.splice(index, 1);
} else {
// Add object.
data.chosenBets.splice(index, 0, obj);
}
}
This does not trigger a re-render in my UI. How do I update the array (in the same way as above) while triggering a re-render?
Just mutating a state won't trigger re-render. You need to call setState() function:
// class component
onClick = () => {
// update state
this.setState(newState);
}
// functional component
...
const [ state, setState ] = useState();
...
setState(newState);
Also, it's quite important to perform immutable state updates since React relies on refs usually (especially, when using memo() or PureComponent). So, it's better to create new instance of array with the same items.
onClick(obj, index) {
let newData;
if (data.chosenBets[index]) {
newData = data.slice();
newData.chosenBets.splice(index, 1);
} else {
newData = [ obj, ...data ];
}
setState(newData);
}
And you always can use some libraties for immutable update like immer, object-path-immutable etc.
Try avoiding impure functions when writing react codes. Here, splice is an impure method. I would recommend using the below code:
onClick(obj, index) {
if (this.state.data.chosenBets[index]) {
// Remove object.
const data = {
...this.state.data,
chosenBets: this.state.data.chosenBets.filter((cBet, i) => i !== index)
};
this.setState({ data });
} else {
// Add object.
const data = {
...this.state.data,
chosenBets: [ ...this.state.data.chosenBets, obj ]
};
this.setState({ data });
}
}
I am assuming you have that array already saved in your state. Then you can do something like this:
onClick = (idx) => {
let arr = [...this.state.arrayToModify];
arr.splice(idx,1);
this.setState({ arrayToModify: arr });
}
Hope this helps!
I needed to make a copy of the array:
let arr = appState.chosenBets
arr.splice(index, 1)
appState.chosenBets = arr
Rather than simply doing
data.chosenBets.splice(index, 1);

How can i edit my code without setState() problem?

I have this code in my constructor:
this.state = {
tests: [
{
question: "1",
answer: "2",
user: ""
},
{
question: "1",
answer: "2",
user: ""
},
],
};
I have edit function where I read event value in my input:
edit(id, event) {
this.state.tests[id].user = event.target.value;
this.setState({tests:this.state.tests});
}
But es give me this warning:
Do not mutate state directly. Use setState()
react/no-direct-mutation-state
What can i do in this case? Maybe somehow change the line with the assignment event.target.value into setState()?
You can use map() to create copy of tests
edit(id, event) {
const user = event.target.value;
const tests = this.state.tests.map((x,i) => i === id ? {...x, user} : x);
this.setState({tests});
}
One way I tend to go is to make a copy of the array first and then change an item in it, or change the array itself, and then set the state
var tests = this.state.tests.slice(0);
tests[id].user = event.target.value;
this.setState({tests:tests});
You may want to deep-clone the array in some cases, sometimes not.
You are correct, that the problem is with the line:
this.state.tests[id].user = event.target.value;
That's the point where you are mutating your state directly.
You have a few options.
You can "clone" the array first and then update it:
const newTests = [...this.state.tests];
newTests[id].user = event.target.value;
this.setState({tests: newTests});
You could also use immutability-helper:
const {value} = event.target;
this.setState(prevState => update(prevState, {[id]: {user: {$set: value}}}));
In this example, you need to extract value from your event, because it's not safe to access event values in asynchronous calls after an event has been handled.
edit(id, event) {
var newNote = {...this.state.tests[id]}
newNote.user = event.target.value
this.setState({ tests: [...this.state.tests, newNote]})
}
First of all, when you try to set a the new state using the data from previous state you have to use the updater as a function
https://reactjs.org/docs/react-component.html#setstate
const edit = (id, event) => {
this.setState((prevState) => {
const tests = [...prevState.tests];
tests[id] = {
...tests[id],
user: event.target.value
};
return {
...prevState,
tests
};
});
};
When state is not heavy, I use the following codes:
edit (id, event) {
const cp = JSON.parse(JSON.stringify(this.state.tests))
cp[id].user = event.target.value
this.setState({ tests: cp })
}
Update: I found Immer solves it perfectly.
import produce from 'immer'
edit (id, event) {
this.setState(
produce(draft => draft.tests[id].user = event.target.value)
)
}

Testing React Components setState overload which takes a function

I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});

Categories