Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: How to update object in array map

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?

like image 566
T. Arafat Avatar asked Feb 17 '26 18:02

T. Arafat


2 Answers

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" />
like image 159
devserkan Avatar answered Feb 19 '26 09:02

devserkan


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>
like image 38
Zdenek F Avatar answered Feb 19 '26 07:02

Zdenek F