3

I'm trying to update the value of an array when a button is clicked. But I can't figure out how to do so using this.setState.

export default class App extends React.Component {
  state = {
    counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
  };
  render() {
    return this.state.counters.map((counter, i) => {
      return (
        <div>
          {counter.name}, {counter.value}
          <button onClick={/* increment counter.value here */}>+</button>
        </div>
      );
    });
  }
}

How do I increment counter.value when the button is clicked?

0

6 Answers 6

4

Update

Since this is the accepted answer now, let me give another optimal method after @Auskennfuchs' comment. Here we use Object.assign and index to update the current counter. With this method, we are avoiding to use unnecessary map over counters.

class App extends React.Component {
  state = {
    counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
  };

  increment = e => {
    const {
      target: {
        dataset: { i }
      }
    } = e;
    const { counters } = this.state;
    const newCounters = Object.assign(counters, {
      ...counters,
      [i]: { ...counters[i], value: counters[i].value + 1 }
    });
    this.setState({ counters: newCounters });
  };

  render() {
    return this.state.counters.map((counter, i) => {
      return (
        <div>
          {counter.name}, {counter.value}
          {/* We are using function reference here */}
          <button data-i={i} onClick={this.increment}>
            +
          </button>
        </div>
      );
    });
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />

As an alternative method, you can use counter name, map over the counters and increment the matched one.

class App extends React.Component {
  state = {
    counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
  };

  increment = name => {
    const newCounters = this.state.counters.map(counter => {
      // Does not match, so return the counter without changing.
      if (counter.name !== name) return counter;
      // Else (means match) return a new counter but change only the value
      return { ...counter, value: counter.value + 1};
    });

    this.setState({counters: newCounters});
  };


  render() {
    return this.state.counters.map((counter, i) => {
      return (
        <div>
          {counter.name}, {counter.value}
          <button onClick={() => this.increment(counter.name)}>+</button>
        </div>
      );
    });
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />

If you use the handler like above it will be recreated on every render. You can either extract it to a separate component or use datasets.

class App extends React.Component {
  state = {
    counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
  };

  increment = e => {
    const {target} = e;
    const newCounters = this.state.counters.map(counter => {
      if (counter.name !== target.dataset.name) return counter;
      return { ...counter, value: counter.value + 1};
    });
    this.setState({counters: newCounters});
  };


  render() {
    return this.state.counters.map((counter, i) => {
      return (
        <div>
          {counter.name}, {counter.value}
          { /* We are using function reference here */ }
          <button data-name={counter.name} onClick={this.increment}>+</button>
        </div>
      );
    });
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />

Sign up to request clarification or add additional context in comments.

2 Comments

Although this is the accepted answer, the implementation is not optimal. The second loop through the whole array inside of increment isn't needed, because the correct index and counter object can be provided to the function itself.
@Auskennfuchs, I've added an alternative method after your comment. Yes, mapping is not needed here. But, I would choose Zdenek F's answer if I were the OP. Since extracting the logic is more optimal I think. This is why I upvoted that answer before OP accepted mine.
3

Consider refactoring the actual counter into its own component. That simplifies the state management as it encapsulates the component responsibility. Suddenly you don't need to update a nested array of objects, but you update just a single state property:

class App extends React.Component {
  state = {
    counters: [{ name: "item1", value: 0 }, { name: "item2", value: 5 }]
  };
  render() {
    return this.state.counters.map((counter, i) => {
      return (
        <Counter name={counter.name} count={counter.value} />
      );
    });
  }
}

class Counter extends React.Component {
  state = {
    count: this.props.count
  }

  increment = () => {
    this.setState({
      count: this.state.count+1
    })
  }

  render() {
      return (
        <div>
          {this.props.name}, {this.state.count}
          <button onClick={this.increment}>+</button>
        </div>
      );
  }
}

ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Comments

0

You could use the i that you already have to map the counters to a new array, replacing the target item with an updated count:

  () => this.setState(({ counters }) => ({ counters: counters.map((prev, i2) => i === i2 ? { ...counter, count: counter.count + 1 } : prev) }))

Comments

0

You can have an handler that will map counters and update the corresponding counters item of the button clicked. Here we take i from the parent scope, and compare it to find the right item to change.

<button onClick={() => {
    this.setState(state => ({
        counters: state.counters.map((item, j) => {
            // is this the counter that I want to update?
            if (j === i) {
                return {
                    ...item,
                    value: item.value + 1
                }
            }

            return item
        })
    }))
}}>+</button>

Comments

0

You can create a click handler like this

handleClick = (index) => {
  this.setState(state => {
    const obj = state.counters[index]; // assign the object at the index to a variable
    obj.value++; // increment the value in the object
    state.counters.splice(index, 1); // remove the object from the array
    return { counters: [...state.counters, obj] };
  });
}

Call it like this... <button onClick={() => handleClick(i)}>

It's possible to make this shorter. Just wanted to explain how you could go about it

Comments

0

With Array.splice you can replace an entry inside of an array. This will return a new array with the replaced value:

    const {counters}= this.state
    return counters.map((counter, i) => (
        <div key={i}>
          {counter.name}, {counter.value}
          <button onClick={() => this.setState({
               counters: counters.splice(i, 1, {
                   ...counter,
                   value: counter.value+1,
               }
             )})}>+</button>
        </div>
   ));

Also it's best practise to give every Fragment inside of a loop it's own unique key. And you can get rid of the return, because there's only the return inside of the map function.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.