I am new to Immutability Helper React library, and I am trying to update multiple state values at the same time, but only the last calling method state is getting updated.
this is what I tried:
state : {l0: null, l1: null}
updateL0 = (l0) => {
if(l0){
this.setState(
update(this.state, {
l0: { $set: l0 }
})
);
}
};
updateL1 = (l1) => {
if(l1){
this.setState(
update(this.state, {
l1: { $set: l1 }
})
);
}
};
Current Output: l1: null, l2: Expected Value
Expected Output: l1: Expected Value , l2: Expected Value
You can't setState multiple times in one event handler this.state may be updated asynchronousl after setState.
From the documentation
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. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
setState will behave like this:
//sate is {name:"Ben",age:22}
this.setState({...this.state,age:23});
console.log(this.state.age);//will log 22
So if you setState multiple times in one event handler you are likely to not get the result you were hoping:
//sate is {name:"Ben",age:22}
this.setState({...this.state,age:23});
console.log(this.state.age);//will log 22
this.setState({...this.state,name:"Harry"});//age will still be 22
The better solution to this is not to use the callback but to write your functions as pure functions (no side effects like setState), pass state to your functions and have them return a new state:
updateL0 = (state,l0) => {
if(l0){
return update(
state,
{
l0: { $set: l0 }
}
);
}
return state;
};
updateL1 = (state,l1) => {
if(l1){
return update(
state,
{
l1: { $set: l1 }
}
);
}
return state;
};
//when you call it you can do:
const newState = updateL0(this.state,L0);
this.setState(updateL1(newState,L1));//note that I'm passing newState here
//or you can just nest updateL1 and updateL0
this.setState(updateL1(updateL0(this.state,L0),L1));
use spread operator (...) in setState method to update multiple state fields as following way
my state is
this.state ={
fields: {
name:'',
email: '',
message: ''
},
errors: {},
disabled : false
}
i'm updating
this.setState(
{...this.state,
fields:{name:'', email: '', message: ''},
disabled: false
});
Related
Hi I am using Formik in my component, where a method call used like below
addMetadata(values) {
console.log(values);
let newState = update(this.state, {
pro: { $set: values}
});
console.log(newState); // this point result print as expected
this.setState(newState);
console.log(this.state); // but here state not showing update result
}
where my state looks like
this.state = {
pro: {
key1: '',
key2: []
key3: {}
}
}
but states are not updating, can anyone know why ?
this.setState(newState) is asynchronous (or at least, it can be). Putting a log statement on the next line will not work, because the state hasn't been set yet.
In the rare cases where you need to know when the setState is done, you can provide a callback as the second argument to setState, which will be called once its complete:
this.setState(
newState,
() => {
console.log(this.state);
}
)
I've been thinking about what would be the best way among these options to update a nested property using React setState() method. I'm also opened to more efficient methods considering performance and avoiding possible conflicts with other possible concurrent state changes.
Note: I'm using a class component that extends React.Component. If you're using React.PureComponent you must be extra careful when updating nested properties because that might not trigger a re-render if you don't change any top-level property of your state. Here's a sandbox illustrating this issue:
CodeSandbox - Component vs PureComponent and nested state changes
Back to this question - My concern here is about performance and possible conflicts between other concurrent setState() calls when updating a nested property on state:
Example:
Let's say I'm building a form component and I will initialize my form state with the following object:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
From my research, by using setState(), React will shallow merge the object that we pass to it, which means that it's only going to check the top level properties, which in this example are isSubmitting and inputs.
So we can either pass it a full newState object containing those two top-level properties (isSubmitting and inputs), or we can pass one of those properties and that will be shallow merged into the previous state.
QUESTION 1
Do you agree that it is best practice to pass only the state top-level property that we are updating? For example, if we are not updating the isSubmitting property, we should avoid passing it to setState() in other to avoid possible conflicts/overwrites with other concurrent calls to setState() that might have been queued together with this one? Is this correct?
In this example, we would pass an object with only the inputs property. That would avoid conflict/overwrite with another setState() that might be trying to update the isSubmitting property.
QUESTION 2
What is the best way, performance-wise, to copy the current state to change its nested properties?
In this case, imagine that I want to set state.inputs.username.touched = true.
Even though you could do this:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
You shouldn't. Because, from React Docs, we have that:
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props.
So, from the excerpt above we can infer that we should build a new object from the current state object, in order to change it and manipulate it as we want and pass it to setState() to update the state.
And since we are dealing with nested objects, we need a way to deep copy the object, and assuming you don't want to use any 3rd party libraries (lodash) to do so, what I've come up with was:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
Note that when your state has nested object you also shouldn't use let newState = Object.assign({},state). Because that would shallow copy the state nested object reference and thus you would still be mutating state directly, since newState.inputs === state.inputs === this.state.inputs would be true. All of them would point to the same object inputs.
But since JSON.parse(JSON.stringify(obj)) has its performance limitations and also there are some data types, or circular data, that might not be JSON-friendly, what other approach would you recommend to deep copy the nested object in order to update it?
The other solution I've come up with is the following:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
What I did in this second alternative was to create an new object from the innermost object that I'm going to update (which in this case is the username object). And I have to get those values inside the key username, and that's why I'm using usernameInput['username'] because later I will merge it into a newInputs object. Everything is done using Object.assign().
This second option has gotten better performance results. At least 50% better.
Any other ideas on this subject? Sorry for the long question but I think it illustrates the problem well.
EDIT: Solution I've adopted from answers below:
My TextInput component onChange event listener (I'm serving it through React Context):
onChange={this.context.onChange(this.props.name)}
My onChange function inside my Form Component
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
I can think of a few other ways to achieve it.
Deconstructing every nested element and only overriding the right one :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
Using the deconstructing operator to copy your inputs :
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
EDIT
First solution using computed properties :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs.[field],
[action]: value
}
}
}))
You can try with nested Object.Assign:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
You can also use spread operator:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
This is proper way to update nested property and keep state immutable.
I made a util function that updates nested states with dynamic keys.
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
You pass a state along with a list of key selectors and the new value.
You can also set the autoAssign value to false to return an object that is a copy of the old state but with the new updated field - otherwise autoAssign = true with update the previous state.
Lastly, if the sequence of selectors don't appear in the object, an object and all nested objects with those keys will be created.
Use the spread operator
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})
I have a function that returns json:
const mapDispatchToProps = dispatch => {
return {
articleDetail: (id) => {
return dispatch(articles.articleDetail(id));
}
}
};
I get the result of the call here:
class ArticleDetail extends Component {
constructor(props) {
super(props);
this.state = {
articleId: props.match.params.id,
asd: "",
art:{}
};
}
componentWillMount() {
this.props.articleDetail(this.state.articleId).then((res) => {
console.log(res.article);
this.setState({art:res.article})
});
this.setState({asd: "asda"})
}
console.log(res.article) return me: {id: 1, author: {…}, headline: "First test article", description: "sadasdsads", img_name: "D.png", …}
but I can't write this result in state, just outside the function, as I did with asd.
I would appreciate it if you would help me, maybe there is some way to write the result of this.props.articleDetail () in state.
I also wanted to ask if I could write the result of calling this function into a variable, and the function returns promise
And also, is it possible to set some variable over this function and record what my console.log "returns" to my external variable.
Thank you so much for your time.
how did you check if the state changed?
In order to properly check if the state has been updated apply a callback to the setState function like this (remember that setState is async):
this.setState({ art: res.article }, () => {
// this happens after the state has been updated
console.log(this.state.art);
});
in regards to your comment about setting the state in the lifecycle methid then it's perfectly fine as long as you do it in componentWillMount and not in componentDidMount.
This is how my state looks like:
constructor(props, context) {
super(props, context);
this.state = {
show: false,
btnLabel: 'GO!',
car: {
owner: false,
manufacturer: false,
color: false
}
};
}
and this is how I modify state:
handleClickFetchPrice() {
this.setState({btnLabel: 'Fetching data...' });
console.log(this.state.fetchPriceBtn);
const url = 'some url';
axios.get(url)
.then(res => {
let car = [...this.state.car];
car.owner = res.data.owner;
car.manufacturer = res.data.manufacturer;
car.color = res.data.color;
this.setState({car});
})
}
The attribute car is updated, but fetchPriceBtn is not - the output of console.log(this.state.fetchPriceBtn); is still GO!.
What am I overlooking? Why the fetchPriceBtn is not updated?
React setState is an asynchronous process - you don't know exactly when it will be updated, you can only schedule the update.
To achieve your desired functionality, you can provide a callback into the setState method.
this.setState({ btnLabel: 'Fetching data...' }, () => console.log(this.state.fetchPriceBtn))
You can learn more following the documentation on the method.
#christopher is right, setState is an asynchronous process. But when second time call handleClickFetchPrice() function your btnLabel is value will be equal to Fetching data...
As answered in previous answers setState is asynchronous, so your console.log can't catch up the state change immediately. Again as suggested you can use callback function to track this change but if you use console.log just for debugging or want to see what changes in your state you can do this in your render function. And using a callback just for debug is not a nice way. Its purpose somehow different and if you check the official documentation, componentDidMount method is being suggested for such logic.
render() {
console.log( this.state.foo );
return (...)
}
If you do that you see two console.log output, one before state change and one after.
Also, your state operations might be enhanced. You car property is not an array, but you are converting it to an array and setting it? Is this what you intend:
axios.get(url)
.then(res => {
const { owner, manufacturer, color } = res.data;
this.setState( prevState => ( { car: { ...prevState.car, owner, manufacturer, color } } ) );
})
Here we are not mutating our state directly, instead we are using spread operator and setting the desired properties. For your example we are setting the whole property actually.
One last note, I think you want to do that something like that:
this.setState( { btnLabel: "fetching } );
axios.get(url)
.then(res => {
const { owner, manufacturer, color } = res.data;
this.setState( prevState => ( { car: { ...prevState.car, owner, manufacturer, color }, btnLabel: "go" } ) );
})
If your intention is somehow to do a status change/check this might no be a good logic as you have seen setState is not synchronous. Do this carefully.
state default values
state = {
moveType: {
value: 0,
open: false,
completed: false
}
};
// callback to update new state
let step = 'moveType';
let val = 3; // new value
let newObj = { ...this.state[step], value: val };
console.log(newObj);
this.setState({[step]: newObj }, function () {console.log(this.state);});
console.log(newObj) shows new values proper, but this.state still shows old values.. can you tell me what i'm doing wrong?
Setting state in react is pretty sensitive thing to do.
The best practices I've used to is always control object deep merge manually and use this.setState(state => { ... return new state; }) type of call, like in this example:
this.setState(state => ({
...state,
[step]: { ...(state[step] || {}), ...newObj },
}), () => console.log(this.state));
SNIPPET UPDATE start
[step]: { ...state[step], ...newObj }
Changed to:
[step]: { ...(state[step] || {}), ...newObj }
To deal correctly with cases, when state does not have this step key yet
SNIPPET UPDATE end
Thing is, that when you use this.state (in let newObj = { ...this.state[step]), it might have an outdated value, due to some pending (not merged yet) changes to the state, that you've called just couple of milliseconds ago.
Thus I recommend to use callback approach: this.setState(state => { ... use state and return new state;}) which guarantees that the state you use has latest value