The react setState after action doesn't work.
handleChange = () => {
this.setState({foo: 'bar'}); < - it working
console.log('hellow') < - does not working, console is clean
}
As far i checked my state, I did everything right about the state values.
I don't know what is the problem
---- update----
The project that created a new create-react-app operates very normally.
I don't know why the above issue occurred, and it doesn't make sense in common sense, but it seems that it's because the project is so messed up.
Thank you for answering such limited situations.
Your code is correct but you need to call handleChange() method
Ex :-
componentDidMount() {
this.handleChange();
}
handleChange = () => {
this.setState({ foo: "bar" });
console.log("Hello");
};
Please try this
handleChange = () => {
this.setState({ foo: "bar" }, () => {
console.log("hellow");
});
};
You can call callback in setState as below
handleChange = () => {
this.setState({foo: 'bar'}, () => {
console.log('hellow')
});
}
Related
I'm very new to React, what I'm trying to achieve is that when I click on a button my location gets detected and updates the state, I've tried doing this however for some reason it is not updating my state, I've searchedall over the internet but couldn't find a solution that matches my business. Your assistance is much appreciated.
this.state = { coordinatesList: [123, 456], [567, 891] }
<button onClick={this.addCurrentLocation}></button>
addCurrentLocation() {
var coordinatesList = this.state.coordinatesList;
console.log("coordinatesList ", coordinatesList)
navigator.geolocation.getCurrentPosition((position) => {
coordinatesList.push([position.coords.latitude, position.coords.longitude]); ==> this doesn't work
})
// coordinatesList.push([123, 456]); ===> this works
this.setState({
coordinatesList
})
}
Your problem is that getCurrentPosition is async so you would have to place the setState inside the callback, otherwise setState gets called before the getCurrentPosition ends.
addCurrentLocation() {
var coordinatesList = this.state.coordinatesList;
navigator.geolocation.getCurrentPosition((position) => {
coordinatesList.push([position.coords.latitude, position.coords.longitude]);
this.setState({
coordinatesList
})
})
}
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)
)
}
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 });
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);
});
I have a function that sets state twice, however - the second setState has to occur after 500ms since first setState has occured (animation purposes).
Code looks like:
const showAnimation = () => {
this.setState({ hidden: false });
setTimeout(() => {
this.setState({ hidden: true });
}, 500);
};
However - if I do it this way, React somehow merges these two setState's into one and my animation doesn't work as expected.
But, if I use a hack:
const showAnimation = () => {
setTimeout(() => {
this.setState({ hidden: false });
}, 0); // ------------------------------> timeout 0
setTimeout(() => {
this.setState({ hidden: true });
}, 500);
};
It works as expected. But still, I don't really like it and Im afraid that it may be some kind of a hack. Is there any better solution for such case? Thanks :)
As setState are async in React you might not get updated state immediately but setState gives you prevState param in setState function to get last updated state so you won't merge state
The syntax goes like this in your case
this.setState((prevState) => { hidden: false }, () => {
setTimeout(() => {
this.setState({ hidden: !prevState.hidden });
}, 500);
});
just update your value to the updated state using prevState
If I understand your problem correct this should work fine
Please let me know if more clarification required
If you try something like that:
const showAnimation = () => {
this.setState({ hidden: false }, () => {
setTimeout(() => {
this.setState({ hidden: true });
}, 500);
}
}
I would personally use animations within JS if you are looking to time it without setTimeout. However this may be down to the face that 'setState' is async within react.
similar :
Why is setState in reactjs Async instead of Sync?
However react does expose a callback within setState - this works for me
this.setState(
{ hidden : false },
() => {
setTimeout(()=>{this.setState({hidden : true})}, 500)
}
);
For rendering performance, react batches calls to setState such that sequential calls will be executed together and will often be reflected in the same render cycle. Per the docs:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
In order to ensure that the first setState has been executed prior to your second call, you can pass setState a callback as the second argument. It's not perfect, but something like the following will ensure that your second call to setState will only happen once hidden: false.
const showAnimation = () => {
this.setState({ hidden: false }, () => setTimeout(() => {
this.setState({ hidden: true });
}, 500););
};