3

I am trying to apply best practices when changing multiple property values of a nested object in my component's state.

My component code currently looks like this:

class App extends Component {
  state = {
    object: {
      a: 10,
      b: 7,
      c: 12,
    }
  }
  render() {
    return (
      <div>
        <button onClick={this.setState({ object: { a: 5 }})}>change value a </button>
        <button onClick={this.setState({ object: { b: 2 }})}>change value b </button>
        <button onClick={this.setState({ object: { c: 3 }})}>change value c </button>
      </div>
      <p>{this.state.object.a}</p>
      <p>{this.state.object.b}</p>
      <p>{this.state.object.c}</p>
    );
  }
}

Before any button is clicked, you see three buttons followed by paragraphs reading 10, 7 and 12 on the page.

Once I click the first button labeled "change value a", value b and c are destroyed in my components state object which causes only the value of 5 to show. If I click the second button, then only 2 will show and prop a and c are gone.

I understand the behaviour, however I would like to know the best way to solve it. I want to keep it so all the values showing, and be able to update them in any order while the other values remain.

I am also pretty sure I am mutating the state directly and I know this is bad practice. I just don't know the right practice in this situation.

Dacre Denny
  • 29,664
  • 5
  • 45
  • 65
Eric Christensen
  • 177
  • 1
  • 16
  • Possible duplicate of [Calling setState in render is not avoidable](https://stackoverflow.com/questions/35290245/calling-setstate-in-render-is-not-avoidable) – Dez Apr 30 '19 at 23:32

2 Answers2

2

First to address what you said, "I am mutating the state directly".

You are are not mutating state directly since you are calling this.setState().

Since you want to update a specific part of object: {}, you can use the spread syntax or Object.assign() as follows:

this.setState({ object: { ...this.state.object, a: 5 } })

or

this.setState({ object: Object.assign({}, this.state.object, { a: 5 }) })

Because you called this.setState() in render, you will get the Maximum call stack exceeded error.

I can think of four ways to solve this, I'll show two of those.

  1. Extract your call into a class method, then pass the reference to the click event handler.
  2. Change this onClick={this.setState({ object: { c: 3 }})} to onClick={() => this.setState({ object: { c: 3 }})}
codejockie
  • 9,020
  • 4
  • 40
  • 46
  • 1
    Using the spread operator work! Thank you! I actually had to full the function out of render though; it was giving me "Maximum update depth exceeded" error – Eric Christensen May 01 '19 at 00:00
1

One way to do this would be by using the spread ... operator on the nested object to merge the update of say { a: 5 }, with the prior state of the object field:

// Create a new state object, with updated value of "5" for nested field "object.a"
{ object: { ...state.object, a: 5 }}

There are a few ways to incorporate this with setState() - one simple way is via a callback passed to setState():

this.setState(state => { object: { ...state.object, a: 5 }})

This allow you to merge the nested objects prior state, with the state changes such as { a : 5 } on object, without completely replacing all the nested object value in your state.

In the case of your render function, you could update the rendered result like so:

 render() {
  return (
  <div>
    <button onClick={ () => this.setState(state => { object: { ...state.object, a: 5 }})}>change value a </button>
    <button onClick={ () => this.setState(state => { object: { ...state.object, b: 2 }})}>change value b </button>
    <button onClick={ () => this.setState(state => { object: { ...state.object, c: 5 }})}>change value c </button>
  </div>
  <p>{this.state.object.a}</p>
  <p>{this.state.object.b}</p>
  <p>{this.state.object.c}</p>
  );
  }
Dacre Denny
  • 29,664
  • 5
  • 45
  • 65