0

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.

Shantanu Bedajna
  • 559
  • 10
  • 34

3 Answers3

3

The reason that newPrice equals 0 after calling setState is because React state updates are asynchronous. The code after the state update will run before the setState actually doing something, so in the stage of calling this.updatePurchaseable(copyState, newPrice); all the calculation of newPrice are yet to be exeuted.

BTW - this is also why your console.log are printed in a "reverse" order, every render the outer log is printed before the inner one.

For this specific code example I’d suggest you to try to move all the calculations that are now in the setState callback outside of it, or even to a different function.

Try this -

calculateNewPrice = (totalPrice, type) => {
    newPrice = totalPrice + PRICES_OF_INGREDIENTS[type];
    newPrice =  Math.round(newPrice * 100) / 100;
}

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

    const newPrice = calculateNewPrice(totalPrice, type);

    this.setState({ ingredients: copyState, totalPrice:  newPrice });
    
    this.updatePurchaseable(copyState, newPrice);
}
arikm9
  • 185
  • 1
  • 13
  • thank you for explaining in deapth, i upvoted, only reason i am not choosing this as the answer cause, i need the anon fun inside setState to get the prevState and @chipit24 provided exactly that, thank you very much !. – Shantanu Bedajna Jul 13 '21 at 17:43
1

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);
}
chipit24
  • 6,509
  • 7
  • 47
  • 67
  • the solution #2 and #3 is really interesting i didn't knew something like #2 is possible, passing an anon func in setState itself to execute when finished and #3 also , amazing !! thanks. – Shantanu Bedajna Jul 13 '21 at 17:46
  • @ShantanuBedajna Careful with this pattern `copyState[type] = oldCount + 1;` as it's *technically* mutating the current state object. Remember that since `copyState` is a shallow copy of the current state that all the deeper references actually still reference back to the object copied from. Remember to stick to functional state updates to correctly update from previous state, such is the case with incrementing counts, and apply immutable update patterns, i.e. shallow copying all nested state that is being updated. – Drew Reese Jul 15 '21 at 04:45
0

React state updates are asynchronous but the setState function is completely synchronous, so newPrice hasn't updated yet when you call updatePurchaseable. Move all the extra "after state update" logic into the componentDidUpdate lifecycle method so you can access/reference the updated totalPrice and call updatePurchaseable with the updated state.

componentDidUpdate(prevProps, prevState) {
  if (prevState.totalPrice !== this.state.totalPrice) {
    const { ingredients, totalPrice } = this.state;
    console.log('newprice outside setState: ' + totalPrice);

    this.updatePurchaseable(ingredients, totalPrice);
  }
}

addIngredientHandler = (type) => {
  this.setState((prevState, prevProps) => {
    let newPrice = prevState.totalPrice + PRICES_OF_INGREDIENTS[type];
    newPrice =  Math.round(newPrice * 100) / 100;
    return {
      ingredients: {
        ...prevState.ingredients,
        [type]: prevState.ingredients[type] + 1,
      }, 
      totalPrice:  newPrice
    }
  });
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181