ReactJS Array.push function not working in setStat

2020-04-05 10:26发布

问题:

I'm making a primitive quiz app with 3 questions so far, all true or false. In my handleContinue method there is a call to push the users input from a radio form into the userAnswers array. It works fine for the first run of handleContinue, after that it throws an error: Uncaught TypeError: this.state.userAnswers.push is not a function(…)

import React from "react"

export default class Questions extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      questionNumber: 1,
      userAnswers: [],
      value: ''
    }

    this.handleContinue = this.handleContinue.bind(this)
    this.handleChange = this.handleChange.bind(this)
  }

  //when Continue button is clicked
  handleContinue() {
    this.setState({
      //this push function throws error on 2nd go round
      userAnswers: this.state.userAnswers.push(this.state.value),
      questionNumber: this.state.questionNumber + 1
    //callback function for synchronicity
    }, () => {
      if (this.state.questionNumber > 3) {
        this.props.changeHeader(this.state.userAnswers.toString())
        this.props.unMount()
      } else {
        this.props.changeHeader("Question " + this.state.questionNumber)
      }
    })
    console.log(this.state.userAnswers)
  }

  handleChange(event) {
    this.setState({
      value: event.target.value
    })
  }

  render() {
    const questions = [
      "Blargh?",
      "blah blah blah?",
      "how many dogs?"
    ]
    return (
      <div class="container-fluid text-center">
        <h1>{questions[this.state.questionNumber - 1]}</h1>
        <div class="radio">
          <label class="radio-inline">
            <input type="radio" class="form-control" name="trueFalse" value="true" 
            onChange={this.handleChange}/>True
          </label><br/><br/>
          <label class="radio-inline">
            <input type="radio" class="form-control" name="trueFalse" value="false" 
            onChange={this.handleChange}/>False
          </label>
          <hr/>
          <button type="button" class="btn btn-primary" 
          onClick={this.handleContinue}>Continue</button>
        </div>
      </div>
    )
  }
}

回答1:

Do not modify state directly! In general, try to avoid mutation.

Array.prototype.push() mutates the array in-place. So essentially, when you push to an array inside setState, you mutate the original state by using push. And since push returns the new array length instead of the actual array, you're setting this.state.userAnswers to a numerical value, and this is why you're getting Uncaught TypeError: this.state.userAnswers.push is not a function(…) on the second run, because you can't push to a number.

You need to use Array.prototype.concat() instead. It doesn't mutate the original array, and returns a new array with the new concatenated elements. This is what you want to do inside setState. Your code should look something like this:

this.setState({
  userAnswers: this.state.userAnswers.concat(this.state.value),
  questionNumber: this.state.questionNumber + 1
}


回答2:

Array.push does not returns the new array. try using

this.state.userAnswers.concat([this.state.value])

this will return new userAnswers array

References: array push and array concat



回答3:

You should treat the state object as immutable, however you need to re-create the array so its pointing to a new object, set the new item, then reset the state.

 handleContinue() {
    var newState = this.state.userAnswers.slice();
    newState.push(this.state.value);

    this.setState({
      //this push function throws error on 2nd go round
      userAnswers: newState,
      questionNumber: this.state.questionNumber + 1
    //callback function for synchronicity
    }, () => {
      if (this.state.questionNumber > 3) {
        this.props.changeHeader(this.state.userAnswers.toString())
        this.props.unMount()
      } else {
        this.props.changeHeader("Question " + this.state.questionNumber)
      }
    })
    console.log(this.state.userAnswers)
  }

Another alternative to the above solution is to use .concat(), since its returns a new array itself. Its equivalent to creating a new variable but is a much shorter code.

this.setState({
  userAnswers: this.state.userAnswers.concat(this.state.value),
  questionNumber: this.state.questionNumber + 1
}


回答4:

The recommended approach in later React versions is to use an updater function when modifying states to prevent race conditions:

this.setState(prevState => ({
  userAnswers: [...prevState.userAnswers, this.state.value] 
}));


回答5:

I have found a solution. This shoud work for splice and others too. Lets say that I have a state which is an array of cars:

this.state = {
  cars: ['BMW','AUDI','mercedes']
};
this.addState = this.addState.bind(this);

Now, addState is the methnod that i will use to add new items to my array. This should look like this:

  addState(){

let arr = this.state.cars;
arr.push('skoda');
this.setState({cars: arr});
}

I have found this solution thanks to duwalanise. All I had to do was to return the new array in order to push new items. I was facing this kind of issue for a lot of time. I will try more functions to see if it really works for all functions that normally won't. If anyone have a better idea how to achieve this with a cleaner code, please feel free to reply to my post.