serraosays
serraosays

Reputation: 7869

Toggling nested state object in React

I have a state object that contains an array of objects:

this.state = {
  feeling: [
    { name: 'alert', status: false },
    { name: 'calm', status: false },
    { name: 'creative', status: false },
    { name: 'productive', status: false },
    { name: 'relaxed', status: false },
    { name: 'sleepy', status: false },
    { name: 'uplifted', status: false }
  ]
}

I want to toggle the boolean status from true to false on click event. I built this function as a click handler but it doesn't connect the event into the state change:

buttonToggle = (event) => {
  event.persist();
  const value = !event.target.value

  this.setState( prevState => ({
    status: !prevState.status
  }))
}

I'm having a hard time following the control flow of the nested React state change, and how the active event makes the jump from the handler to the state object and vice versa.

The whole component:

export default class StatePractice extends React.Component {

  constructor() {
    super();
    this.state = {
      feeling: [
        { name: 'alert', status: false },
        { name: 'calm', status: false },
        { name: 'creative', status: false },
        { name: 'productive', status: false },
        { name: 'relaxed', status: false },
        { name: 'sleepy', status: false },
        { name: 'uplifted', status: false }
      ]
    }
  }

  buttonToggle = (event) => {
    event.persist();
    const value = !event.target.value

    this.setState( prevState => ({
      status: !prevState.status
    }))
  }



  render() {
    return (  
      <div>
        { this.state.feeling.map(
            (stateObj, index) => { 
              return <button 
                key={ index }
                onClick={ this.buttonToggle } 
                value={ stateObj.status } >
                  { stateObj.status.toString() }
                </button>
            }
          )
        }
      </div>
    )
  }
}  

Upvotes: 1

Views: 2348

Answers (3)

duc mai
duc mai

Reputation: 1422

I think you need to update the whole array when get the event. And it is better to not mutate the existing state. I would recommend the following code

export default class StatePractice extends React.Component {
  constructor() {
    super();
    this.state = {
      feeling: [
        { name: "alert", status: false },
        { name: "calm", status: false },
        { name: "creative", status: false },
        { name: "productive", status: false },
        { name: "relaxed", status: false },
        { name: "sleepy", status: false },
        { name: "uplifted", status: false },
      ],
    };
  }

  buttonToggle = (index, value) => (event) => {
    event.persist();
    const toUpdate = { ...this.state.feeling[index], status: !value };
    const feeling = [...this.state.feeling];
    feeling.splice(index, 1, toUpdate);
    this.setState({
      feeling,
    });
  };

  render() {
    return (
      <div>
        {this.state.feeling.map((stateObj, index) => {
          return (
            <button
              key={index}
              onClick={this.buttonToggle(index, stateObj.status)}
              value={stateObj.status}
            >
              {stateObj.status.toString()}
            </button>
          );
        })}
      </div>
    );
  }
}

Upvotes: 0

Treycos
Treycos

Reputation: 7492

In order to solve your problem, you should first send the index of the element that is going to be modified to your toggle function :

onClick = {this.buttonToggle(index)} 

Then tweak the function to receive both the index and the event.

Now, to modify your state array, copy it, change the value you are looking for, and put it back in your state :

buttonToggle = index => event => {
    event.persist();
    const feeling = [...this.state.feeling]; //Copy your array
    feeling[index] = !feeling[index];
    this.setState({ feeling }); 
}

You can also use slice to copy your array, or even directly send a mapped array where only one value is changed.

Upvotes: 2

Robb216
Robb216

Reputation: 552

Updating a nested object in a react state object is tricky. You have to get the entire object from the state in a temporary variable, update the value within that variable and then replace the state with the updated variable. To do that, your buttonToggle function needs to know which button was pressed.

return <button 
  key={ index }
  onClick={ (event) => this.buttonToggle(event, stateObj.name) } 
  value={ stateObj.status } >
    { stateObj.status.toString() }
  </button>

And your buttonToggle function could look like this

buttonToggle = (event, name) => {
  event.persist();

  let { feeling } = this.state;
    let newFeeling = [];

  for (let index in feeling) {
    let feel = feeling[index];
    if (feel.name == name) {
        feel = {name: feel.name, status: !feel.status};
    }
    newFeeling.push(feel);
  }

  this.setState({
    feeling: newFeeling,
  });
}

Here's a working JSFiddle.

Alternatively, if you don't need to store any more data per feeling than "name" and "status", you could rewrite your component state like this:

 feeling: {
    alert: false,
    calm: false,
    creative: false,
    etc...
  }

And buttonToggle:

 buttonToggle = (event, name) => {
   event.persist();
   let { feeling } = this.state;
   feeling[name] = !feeling[name];
   this.setState({
    feeling
   });
 }

Upvotes: 1

Related Questions