Home » Reactjs » Correct modification of state arrays in ReactJS

Correct modification of state arrays in ReactJS

Posted by: admin November 29, 2017 Leave a comment

Questions:

I want to add an element to the end of a state array, is this the correct way to do it?

this.state.arrayvar.push(newelement);
this.setState({arrayvar:this.state.arrayvar});

I am concerned that modifying the array in-place with push might cause trouble – is it safe?

The alternative of making a copy of the array, and setStateing that seems wasteful.

Answers:

The React docs says:

Treat this.state as if it were immutable.

Your push will mutate the state directly and that could potentially lead to error prone code, even if you are “resetting” the state again afterwards. F.ex, it could lead to that some lifecycle methods like componentDidUpdate won’t trigger.

A better option would be as you mentioned:

var arrayvar = this.state.arrayvar.slice()
arrayvar.push(newelement)
this.setState({ arrayvar: arrayvar })

The memory “waste” is not an issue compared to the errors you might face using non-standard state modifications.

Alternative syntax

You can use concat to get a cleaner syntax since it returns a new array:

this.setState({ 
  arrayvar: this.state.arrayvar.concat([newelement])
})

In ES6 you can use the Spread Operator:

this.setState({
  arrayvar: [...this.state.arrayvar, newelement]
})

In later React versions, the recommended approach is to use an updater function instead of an object which allows a more functional syntax:

this.setState(prevState => ({
  arrayvar: [...prevState.arrayvar, newelement]
}))

Questions:
Answers:

Easiest, if you are using ES6.

initialArray = [1, 2, 3];

newArray = [ ...initialArray, 4 ]; // --> [1,2,3,4]

New array will be [1,2,3,4]

to update your state in React

this.setState({arrayvar:[...this.state.arrayvar, newelement]});

Learn more about array destructuring

Questions:
Answers:

As @nilgun mentioned in the comment, you can use the react immutability helpers. I’ve found this to be super useful.

From the docs:

Simple push

var initialArray = [1, 2, 3];
var newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]

initialArray is still [1, 2, 3].

Questions:
Answers:

React may batch updates, and therefore the correct approach is to provide setState with a function that performs the update.

For the React update addon, the following will reliably work:

this.setState( (state) => update(state, {array: {$push: [4]}}) );

or for concat():

this.setState( (state) => {
    state.array = state.array.concat([4]);
    return state;
});

The following shows what https://jsbin.com/mofekakuqi/7/edit?js,output as an example of what happens if you get it wrong.

The setTimeout() invocation correctly adds three items because React will not batch updates within a setTimeout callback (see https://groups.google.com/d/msg/reactjs/G6pljvpTGX0/0ihYw2zK9dEJ).

The buggy onClick will only add “Third”, but the fixed one, will add F, S and T as expected.

class List extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      array: []
    }

    setTimeout(this.addSome, 500);
  }

  addSome = () => {
      this.setState(
        update(this.state, {array: {$push: ["First"]}}));
      this.setState(
        update(this.state, {array: {$push: ["Second"]}}));
      this.setState(
        update(this.state, {array: {$push: ["Third"]}}));
    };

  addSomeFixed = () => {
      this.setState( (state) => 
        update(state, {array: {$push: ["F"]}}));
      this.setState( (state) => 
        update(state, {array: {$push: ["S"]}}));
      this.setState( (state) => 
        update(state, {array: {$push: ["T"]}}));
    };



  render() {

    const list = this.state.array.map((item, i) => {
      return <li key={i}>{item}</li>
    });
       console.log(this.state);

    return (
      <div className='list'>
        <button onClick={this.addSome}>add three</button>
        <button onClick={this.addSomeFixed}>add three (fixed)</button>
        <ul>
        {list}
        </ul>
      </div>
    );
  }
};


ReactDOM.render(<List />, document.getElementById('app'));

Questions:
Answers:

The simplest way with ES6:

this.setState(prevState => ({
    array: [...prevState.array, newElement]
}))

Questions:
Answers:

If you are willing to use push you can do something like this:

// wrong because push modify the array directly
this.setState(prevState => ({
  arrayvar: prevState.arrayvar.push(someElemet) && prevState.arrayvar,
}));

A better solution of course is to avoid mutation by using concat, slice or spread.

See this video tutorial for more explanation.

Questions:
Answers:

this is my code to deal with this issue, my verdict is: Reactjs is junk. A simple change state is so much trouble, unless you use some exotic store solution. Don’t use ReactJS.

  setQuestion(e) {

      const questionIndex = parseInt( e.target.dataset.index )


      let questions = this.state.formPatientQuestions.slice()
      questions[questionIndex].question = e.target.value
      this.setState({formPatientQuestions: questions})

      if (questionIndex === this.state.formPatientQuestions.length - 1 ) {

        let qs = this.state.formPatientQuestions.slice()
        qs.push({question:'', answer: ''})
        this.setState({formPatientQuestions: qs})
      }

      if (e.target.value.length === 0) {
        let qs = this.state.formPatientQuestions.slice()
        qs.splice(questionIndex, 1)
        this.setState({formPatientQuestions: qs})

      }

      this.localCase.questionAnswers = this.state.formPatientQuestions
      this.setState({ refresh: true });
      this.canCreate();
  }