Skip to content
Advertisement

Function variable value resetting after calling this.setState

I am relatively new to the JavaScript world, i am learning react and have encountered a weird issue see this code

addIngredientHandler = (type) => {

    let oldCount  = this.state.ingredients[type];
    let copyState = {...this.state.ingredients};

    let newPrice = 0;

    copyState[type] = oldCount + 1;

    this.setState( (prevState, prevProps) => {

        newPrice = prevState.totalPrice + PRICES_OF_INGREDIENTS[type];

        newPrice =  Math.round(newPrice * 100) / 100;

        console.log('newprice inside setState: ' + newPrice);
        
        return { ingredients: copyState, totalPrice:  newPrice}
        

    } );

    console.log('newprice outside setState: ' + newPrice);

    this.updatePurchaseable(copyState, newPrice);


}

here i am concerned with the newPrice variable which is used update the state when more items are added, which works fine

problem is after the this.setState return the newPrice gets retested to 0 again so i can’t use it for the function at the bottom.

Yes i can use the state variable directly but due to the asnyc nature of setState execution i wanted to pass the variable value instead.

in the console you can see that first the outer console log gets executed then the inside one due to async nature of setState

enter image description here

maybe i am not getting some lifecycle react has that is generating this type of behavior.

here is the state values, in the values shouldn’t matter but still for a better picture

state = {
    ingredients: {
        salad: 0,
        bacon: 0,
        meat: 0,
        cheese: 0,
    },
    purchasable: false,

    totalPrice: 0

}

Any hint helps, thanks for reading.

Advertisement

Answer

this.setState() gets called asynchronously so you cannot rely on this.state referencing the updated value immediately after calling this.setState(). Have a read through the FAQ on component state.

If you want to reference the updated value of newPrice after the state has been updated, you can:

  1. Use the componentDidUpdate() lifecycle method. See https://reactjs.org/docs/react-component.html#componentdidupdate.
addIngredientHandler = (type) => {
  let oldCount = this.state.ingredients[type];
  let copyState = { ...this.state.ingredients };

  let newPrice = 0;

  copyState[type] = oldCount + 1;

  this.setState((prevState) => {
    newPrice = prevState.totalPrice + PRICES_OF_INGREDIENTS[type];
    newPrice = Math.round(newPrice * 100) / 100;

    return { ingredients: copyState, totalPrice: newPrice }
  });
}

componentDidUpdate(prevProps, prevState) {
  if (prevState.totalPrice !== this.state.totalPrice) {
    this.updatePurchaseable(this.state.ingredients, this.state.totalPrice);
  }
}
  1. Use the 2nd argument to this.setState(). See the docs at https://reactjs.org/docs/react-component.html#setstate.
addIngredientHandler = (type) => {
  let oldCount = this.state.ingredients[type];
  let copyState = { ...this.state.ingredients };

  let newPrice = 0;

  copyState[type] = oldCount + 1;

  this.setState((prevState) => {
    newPrice = prevState.totalPrice + PRICES_OF_INGREDIENTS[type];
    newPrice = Math.round(newPrice * 100) / 100;

    return { ingredients: copyState, totalPrice: newPrice }
  }, () => {
    this.updatePurchaseable(this.state.ingredients, this.state.totalPrice);
  });
}
  1. Use ReactDOM.flushSync(). See https://github.com/reactwg/react-18/discussions/21.
import { flushSync } from 'react-dom';

addIngredientHandler = (type) => {
  let oldCount = this.state.ingredients[type];
  let copyState = { ...this.state.ingredients };

  let newPrice = 0;

  copyState[type] = oldCount + 1;

  flushSync(() => {
    this.setState((prevState) => {
      newPrice = prevState.totalPrice + PRICES_OF_INGREDIENTS[type];
      newPrice = Math.round(newPrice * 100) / 100;

      return { ingredients: copyState, totalPrice: newPrice }
    });
  });

  this.updatePurchaseable(copyState, newPrice);
}

If I were to write this method, I would recommend using the componentDidUpdate lifecycle method as this will ensure updatePurchaseable is always called when the total price changes. If you only call updatePurchaseable inside your event handler, then you may end up with a bug if the price changes outside of that handler.

addIngredientHandler = (type) => {
  this.setState(prevState => {
    let totalPrice = prevState.totalPrice + PRICES_OF_INGREDIENTS[type];
    totalPrice = Math.round(totalPrice * 100) / 100;

    return {
      ingredients: {
        ...prevState.ingredients,
        [type]: prevState.ingredients[type] + 1,
      },
      totalPrice,
    };
  });
}

componentDidUpdate(prevProps, prevState) {
  const { totalPrice, ingredients } = this.state;

  if (prevState.totalPrice === totalPrice) {
    /*
    
    Bail early. This is a personal code style preference. It may 
    make things easier to read as it keeps the main logic on the 
    "main line" (un-nested / unindented)
    
    */
    return;
  }

  /*

  If `updatePurchaseable` is a class method then you don't need to
  pass state to it as it will already have access to `this.state`.

  If `updatePurchaseable` contains complicated business logic,
  consider pulling it out into its own module to make it easier 
  to test.
  
  */
  this.updatePurchaseable(ingredients, totalPrice);
}
Advertisement