344

Is it at all possible to update object's properties with setState?

Something like:

this.state = {
   jasper: { name: 'jasper', age: 28 },
}

I have tried:

this.setState({jasper.name: 'someOtherName'});

and this:

this.setState({jasper: {name: 'someothername'}})

The first results in a syntax error and the second just does nothing. Any ideas?

norbitrial
  • 12,734
  • 5
  • 20
  • 46
JohnSnow
  • 4,775
  • 7
  • 16
  • 37
  • 8
    second code would have worked however you would have lost the `age` property inside `jasper`. – gmoniava Jul 10 '18 at 08:14
  • I understand that React uses .assign() to. merge the old state object with the new object so shouldn't the second code work properly? – Saurabh Rana Feb 25 '21 at 03:39

17 Answers17

770

There are multiple ways of doing this, since state update is a async operation, so to update the state object, we need to use updater function with setState.

1- Simplest one:

First create a copy of jasper then do the changes in that:

this.setState(prevState => {
  let jasper = Object.assign({}, prevState.jasper);  // creating copy of state variable jasper
  jasper.name = 'someothername';                     // update the name property, assign a new value                 
  return { jasper };                                 // return new object jasper object
})

Instead of using Object.assign we can also write it like this:

let jasper = { ...prevState.jasper };

2- Using spread syntax:

this.setState(prevState => ({
    jasper: {                   // object that we want to update
        ...prevState.jasper,    // keep all other key-value pairs
        name: 'something'       // update the value of specific key
    }
}))

Note: Object.assign and Spread Operator creates only shallow copy, so if you have defined nested object or array of objects, you need a different approach.


Updating nested state object:

Assume you have defined state as:

this.state = {
  food: {
    sandwich: {
      capsicum: true,
      crackers: true,
      mayonnaise: true
    },
    pizza: {
      jalapeno: true,
      extraCheese: false
    }
  }
}

To update extraCheese of pizza object:

this.setState(prevState => ({
  food: {
    ...prevState.food,           // copy all other key-value pairs of food object
    pizza: {                     // specific object of food object
      ...prevState.food.pizza,   // copy all pizza key-value pairs
      extraCheese: true          // update value of specific key
    }
  }
}))

Updating array of objects:

Lets assume you have a todo app, and you are managing the data in this form:

this.state = {
  todoItems: [
    {
      name: 'Learn React Basics',
      status: 'pending'
    }, {
      name: 'Check Codebase',
      status: 'pending'
    }
  ]
}

To update the status of any todo object, run a map on the array and check for some unique value of each object, in case of condition=true, return the new object with updated value, else same object.

let key = 2;
this.setState(prevState => ({

  todoItems: prevState.todoItems.map(
    el => el.key === key? { ...el, status: 'done' }: el
  )

}))

Suggestion: If object doesn't have a unique value, then use array index.

Mayank Shukla
  • 80,295
  • 14
  • 134
  • 129
  • The way I was trying now works though... the second way :S – JohnSnow Apr 26 '17 at 16:05
  • 9
    @JohnSnow in you second it will remove the other properties from `jasper` object, do the `console.log(jasper)` you will see only one key `name`, age will not be there :) – Mayank Shukla Apr 26 '17 at 16:07
  • True... Is the way you described the ONLY way to go about it... considering I want to use React and not Redux? – JohnSnow Apr 26 '17 at 16:08
  • 1
    @JohnSnow, yes this way is proper if `jasper` is an `object`, don't the whether best or not, may be other better solutions are possible :) – Mayank Shukla Apr 26 '17 at 16:16
  • Slightly more elegant ES6+ solution: [with spread operator](https://stackoverflow.com/a/34956745/2984693). – Xarvalus Sep 07 '17 at 13:06
  • 7
    Good to mention here that neither `Object.assign` nor spread operator deep copy properties. In this way you should use workarounds like lodash `deepCopy` etc. – Alex Vasilev May 24 '18 at 08:08
  • this.state.jasper isn't guaranteed to contain the object version you expect, right? (Referring to another answer's comment: "...in order to optimize state updates React might group multiple updates. As a consequence this.state.jasper does not necessarily contain the latest state.") – Ben Wheeler Jun 21 '18 at 14:08
  • @giorgim its not a nested object, Object.assign will create a copy of the object, so `jasper.name = 'someothername';` will not affect the original object. better to try the same in console and see the output :) – Mayank Shukla Jul 10 '18 at 07:16
  • @MayankShukla ok, i see now; if `jasper` had object inside then modifying that object would not be correct – gmoniava Jul 10 '18 at 08:01
  • I use spread operator. I added the input value to the variable `name` and use it as follows: `jasper.name: name` – Troyan Victor Oct 03 '18 at 09:43
  • 1
    How bout `let { jasper } = this.state`? – Brian Le Nov 23 '18 at 20:44
  • To deep copy an object (and all properties), you could always do: `JSON.parse(JSON.stringify(nestedObjToCopy))` – Ben Pritchard Dec 12 '19 at 00:04
  • Is it mandatory to use the spread operator to create a new object, instead of updating the existing one? – ankur_rajput Aug 28 '20 at 11:09
  • 1
    @ankur_rajput we use spread operator to create a shallow copy, you can use Object.assign as well. Avoid the direct mutation of the state object. – Mayank Shukla Aug 31 '20 at 06:53
  • @T.J.Crowder haha, thanks :) – Mayank Shukla Feb 06 '21 at 11:53
  • 1
    I find this so cumbersome pluming so much code for evt. just updating a single element in an array in a state object or like!, Seems like a lack in the language, that there's not a simple way to do this SO trivial thing. It is so error prone as well, do a little mistake and ups, there you lost a property. I really hope someone will evt. create better implementations for these operations, either as helper functions in state management end, or maybe even a version of spread operator which can do nested update/copy. – Peter Stjernholm Meldgaard Mar 02 '21 at 21:40
48

This is the fastest and the most readable way:

this.setState({...this.state.jasper, name: 'someothername'});

Even if this.state.jasper already contains a name property, the new name name: 'someothername' with be used.

Tim Gerhard
  • 2,751
  • 1
  • 13
  • 35
mannok
  • 948
  • 1
  • 8
  • 20
  • 52
    This solution has one big disadvantage: in order to optimize state updates React might group multiple updates. As a consequence `this.state.jasper` does not necessarily contain the latest state. Better use the notation with the `prevState`. – sinned Jun 08 '18 at 07:55
35

Use spread operator and some ES6 here

this.setState({
    jasper: {
          ...this.state.jasper,
          name: 'something'
    }
})
Just code
  • 12,567
  • 10
  • 45
  • 82
Nikhil
  • 927
  • 9
  • 15
21

I know there are a lot of answers here, but I'm surprised none of them create a copy of the new object outside of setState, and then simply setState({newObject}). Clean, concise and reliable. So in this case:

const jasper = { ...this.state.jasper, name: 'someothername' }
this.setState(() => ({ jasper }))

Or for a dynamic property (very useful for forms)

const jasper = { ...this.state.jasper, [VarRepresentingPropertyName]: 'new value' }
this.setState(() => ({ jasper }))
colemerrick
  • 895
  • 10
  • 15
11

I used this solution.

If you have a nested state like this:

this.state = {
  formInputs:{
    friendName:{
      value:'',
      isValid:false,
      errorMsg:''
    },
    friendEmail:{
      value:'',
      isValid:false,
      errorMsg:''
    }
  }
}

you can declare the handleChange function that copy current status and re-assigns it with changed values

handleChange(el) {
    let inputName = el.target.name;
    let inputValue = el.target.value;

    let statusCopy = Object.assign({}, this.state);
    statusCopy.formInputs[inputName].value = inputValue;

    this.setState(statusCopy);
  }

here the html with the event listener. Make sure to use the same name used into state object (in this case 'friendName')

<input type="text" onChange={this.handleChange} " name="friendName" />
ravibagul91
  • 16,494
  • 4
  • 24
  • 45
Alberto Piras
  • 421
  • 5
  • 6
  • This worked for me, except I had to use this instead: `statusCopy.formInputs[inputName] = inputValue;` – MikeyE Jan 02 '20 at 00:26
7

try this,it should work fine

this.setState(Object.assign(this.state.jasper,{name:'someOtherName'}));
  • 5
    You are mutating the state object directly. To use this approach, add a new object as the source : ```Object.assign({}, this.state.jasper, {name:'someOtherName'})``` – Albizia Jul 24 '19 at 18:33
4

this is another solution using immer immutabe utility, very suited for deeply nested objects with ease, and you should not care about mutation

this.setState(
    produce(draft => {
       draft.jasper.name = 'someothername'
    })
)
samehanwar
  • 1,739
  • 2
  • 9
  • 18
3

The first case is indeed a syntax error.

Since I can't see the rest of your component, it's hard to see why you're nesting objects in your state here. It's not a good idea to nest objects in component state. Try setting your initial state to be:

this.state = {
  name: 'jasper',
  age: 28
}

That way, if you want to update the name, you can just call:

this.setState({
  name: 'Sean'
});

Will that achieve what you're aiming for?

For larger, more complex data stores, I would use something like Redux. But that's much more advanced.

The general rule with component state is to use it only to manage UI state of the component (e.g. active, timers, etc.)

Check out these references:

mccambridge
  • 910
  • 7
  • 16
  • 1
    I only used that as an example, the object must be nested.. I probably should be using Redux but I am trying to understand React fundementals.. – JohnSnow Apr 26 '17 at 16:07
  • 2
    Yeah, I would stay away from Redux for now. While you're learning the fundamentals, try to keep your data simple. That'll help you avoid odd cases that trip you up. – mccambridge Apr 26 '17 at 16:39
2

Another option: define your variable out of the Jasper object and then just call a variable.

Spread operator: ES6

this.state = {  jasper: { name: 'jasper', age: 28 } } 

let foo = "something that needs to be saved into state" 

this.setState(prevState => ({
    jasper: {
        ...jasper.entity,
        foo
    }
})
J. Steen
  • 14,900
  • 15
  • 57
  • 62
Luis Martins
  • 1,124
  • 8
  • 11
2

You can try with this: (Note: name of input tag === field of object)

<input name="myField" type="text" 
      value={this.state.myObject.myField} 
     onChange={this.handleChangeInpForm}>
</input>

-----------------------------------------------------------
handleChangeInpForm = (e) => {
   let newObject = this.state.myObject;
   newObject[e.target.name] = e.target.value;
   this.setState({
     myObject: newObject 
   })
}
ravibagul91
  • 16,494
  • 4
  • 24
  • 45
  • 1
    Welcome to Stack Overflow. While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. [How to Answer](https://stackoverflow.com/help/how-to-answer) – Elletlar Jan 28 '19 at 12:51
2

Simple and dynamic way.

This will do the job, but you need to set all the ids to the parent so the parent will point to the name of the object, being id = "jasper" and name the name of the input element = property inside of the object jasper.

handleChangeObj = ({target: { id , name , value}}) => this.setState({ [id]: { ...this.state[id] , [name]: value } });
  • I agree with first line, as this is the only thing that worked for my object of objects in state! Also it cleared where I was stuck in updating my data. By far the most simple and dynamic way... Thanks !! – Smit Dec 15 '19 at 16:28
2

Without using Async and Await Use this...

funCall(){    
     this.setState({...this.state.jasper, name: 'someothername'});
}

If you using with Async And Await use this...

async funCall(){
      await this.setState({...this.state.jasper, name: 'someothername'});
}
Shantanu Sharma
  • 436
  • 4
  • 13
1

Also, following Alberto Piras solution, if you don't want to copy all the "state" object:

handleChange(el) {
    let inputName = el.target.name;
    let inputValue = el.target.value;

    let jasperCopy = Object.assign({}, this.state.jasper);
    jasperCopy[inputName].name = inputValue;

    this.setState({jasper: jasperCopy});
  }
Marc LaQuay
  • 190
  • 1
  • 6
1

You can try with this:

this.setState(prevState => {
   prevState = JSON.parse(JSON.stringify(this.state.jasper));
   prevState.name = 'someOtherName';
   return {jasper: prevState}
})

or for other property:

this.setState(prevState => {
   prevState = JSON.parse(JSON.stringify(this.state.jasper));
   prevState.age = 'someOtherAge';
   return {jasper: prevState}
})

Or you can use handleChage function:

handleChage(event) {
   const {name, value} = event.target;
    this.setState(prevState => {
       prevState = JSON.parse(JSON.stringify(this.state.jasper));
       prevState[name] = value;
       return {jasper: prevState}
    })
}

and HTML code:

<input 
   type={"text"} 
   name={"name"} 
   value={this.state.jasper.name} 
   onChange={this.handleChange}
/>
<br/>
<input 
   type={"text"} 
   name={"age"} 
   value={this.state.jasper.age} 
   onChange={this.handleChange}
/>
  • This works without JSON.stringify .. this.setState(prevState => { prevState = this.state.document; prevState.ValidDate = event.target.value; prevState.ValidDateFormat = dayFormat; return {document: prevState} }); ..Where document is state type object.. – Oleg Borodko Jun 17 '19 at 15:35
1

Try with this:

const { jasper } = this.state; //Gets the object from state
jasper.name = 'A new name'; //do whatever you want with the object
this.setState({jasper}); //Replace the object in state
David Jesus
  • 1,041
  • 2
  • 17
  • 24
0

This setup worked for me:

let newState = this.state.jasper;
newState.name = 'someOtherName';

this.setState({newState: newState});

console.log(this.state.jasper.name); //someOtherName
LaZza
  • 140
  • 1
  • 5
0

Your second approach doesn't work because {name: 'someothername'} equals {name: 'someothername', age: undefined}, so theundefined would overwrite original age value.

When it comes to change state in nested objects, a good approach would be Immutable.js

this.state = {
  jasper: Record({name: 'jasper', age: 28})
}

const {jasper} = this.state
this.setState({jasper: jasper.set(name, 'someothername')})
nolan
  • 391
  • 2
  • 10