XEnterprise
XEnterprise

Reputation: 382

React hook state one step back and not updating

I am trying to create an average calculator, My OnChange events on dropdowns and text fields update the hook value, but when I try to get the final value, But to calculate I now need to press the button 2 times otherwise it does not consider the latest state in hooks, Please let me know what I am missing here, I have tried to do it using async await but no benefit. Following is the code

    import React from 'react';
    import { makeStyles } from '@material-ui/core/styles';
    import InputLabel from '@material-ui/core/InputLabel';
    import MenuItem from '@material-ui/core/MenuItem';
    import FormHelperText from '@material-ui/core/FormHelperText';
    import FormControl from '@material-ui/core/FormControl';
    import Select from '@material-ui/core/Select';
    import TextField from '@material-ui/core/TextField';
    import Button from '@material-ui/core/Button';
    
    
    const useStyles = makeStyles((theme) => ({
        root: {
            '& .MuiTextField-root': {
                margin: theme.spacing(1),
                width: '25ch',
            },
        },
        formControl: {
            margin: theme.spacing(1),
            minWidth: 120,
        },
        selectEmpty: {
            marginTop: theme.spacing(2),
        },
    }));
    
    export default function AverageCalculator() {
        const classes = useStyles();
    
        const [price1, setPrice1] = React.useState(7500);
        const [price2, setPrice2] = React.useState(7300);
        const [price3, setPrice3] = React.useState(7200);
        const [price4, setPrice4] = React.useState(0);
    
        const [percentage1, setPercentage1] = React.useState(1);
        const [percentage2, setPercentage2] = React.useState(2);
        const [percentage3, setPercentage3] = React.useState(1);
        const [percentage4, setPercentage4] = React.useState(1);
    
        const [portfolioUsed, setportfolioUsed] = React.useState(0);
        const [avgPrice, setAvgPrice] = React.useState(0);
    
        const [update, setUpdate] = React.useState(true);
    
        const handleApply = (event) => {
            setUpdate(!update)
            console.log("PPPPP", update)
        }
    
        React.useEffect(async() => {
            await setportfolioUsed((price1 > 0 ? percentage1 : 0) + (price2 > 0 ? percentage2 : 0) + (price3 > 0 ? percentage3 : 0) + (price4 > 0 ? percentage4 : 0))
            await setAvgPrice((price1 * percentage1 + price2 * percentage2 + price3 * percentage3 + price4 * percentage4) / portfolioUsed)
        }, [update])
    
        return (
            <div>
                <form className={classes.root} noValidate autoComplete="off">
                    <TextField id="price1" value={price1} label="Primary Buying" onChange={(e) => setPrice1(e.target.value)} />
                    <TextField
                        id="perc1"
                        select
                        label="Portfolio"
                        value={percentage1}
                        onChange={(e) => setPercentage1(e.target.value)}
                    >
                        <MenuItem value={0}>0%</MenuItem>
                        <MenuItem value={1}>25%</MenuItem>
                        <MenuItem value={2}>50%</MenuItem>
                        <MenuItem value={3}>75%</MenuItem>
                        <MenuItem value={4}>100%</MenuItem>
                    </TextField>
                </form>
                <form className={classes.root} noValidate autoComplete="off">
                    <TextField id="price2" value={price2} label="Backup 1" onChange={(e) => setPrice2(e.target.value)} />
                    <TextField
                        id="perc2"
                        select
                        label="Portfolio"
                        value={percentage2}
                        onChange={(e) => setPercentage2(e.target.value)}
                    >
                        <MenuItem value={0}>0%</MenuItem>
                        <MenuItem value={1}>25%</MenuItem>
                        <MenuItem value={2}>50%</MenuItem>
                        <MenuItem value={3}>75%</MenuItem>
                        <MenuItem value={4}>100%</MenuItem>
                    </TextField>
                </form>
                <form className={classes.root} noValidate autoComplete="off">
                    <TextField id="price3" value={price3} label="Backup 2" onChange={(e) => setPrice3(e.target.value)} />
                    <TextField
                        id="perc3"
                        select
                        label="Portfolio"
                        value={percentage3}
                        onChange={(e) => setPercentage3(e.target.value)}
                    >
                        <MenuItem value={0}>0%</MenuItem>
                        <MenuItem value={1}>25%</MenuItem>
                        <MenuItem value={2}>50%</MenuItem>
                        <MenuItem value={3}>75%</MenuItem>
                        <MenuItem value={4}>100%</MenuItem>
                    </TextField>
                </form>
                <form className={classes.root} noValidate autoComplete="off">
                    <TextField id="price4" value={price4} label="Backup 3" onChange={(e) => setPrice4(e.target.value)} />
                    <TextField
                        id="perc4"
                        select
                        label="Portfolio"
                        value={percentage4}
                        onChange={(e) => setPercentage4(e.target.value)}
                    >
                        <MenuItem value={0}>0%</MenuItem>
                        <MenuItem value={1}>25%</MenuItem>
                        <MenuItem value={2}>50%</MenuItem>
                        <MenuItem value={3}>75%</MenuItem>
                        <MenuItem value={4}>100%</MenuItem>
                    </TextField>
                </form>
                <p>{portfolioUsed > 4 ? "Your portfolio percentage selection is wrong" : "Average Price: " + avgPrice}</p>
    
                <Button variant="contained" color="primary" onClick={()=>setUpdate(prevState=>!prevState)}>
                    Apply
                </Button>
    
            </div>
        );
    }

Upvotes: 0

Views: 163

Answers (2)

Rishabh Singh
Rishabh Singh

Reputation: 391

This is due to the fact that useState is asyn in nature but you cannot simply await it to get a sync behaviour. Also as @rubendmatos1985 mentioned, you should not pass asyn function to useEffect.

As far as fixing goes you can simply get around the problem like so

import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";

const useStyles = makeStyles((theme) => ({
  root: {
    "& .MuiTextField-root": {
      margin: theme.spacing(1),
      width: "25ch"
    }
  },
  formControl: {
    margin: theme.spacing(1),
    minWidth: 120
  },
  selectEmpty: {
    marginTop: theme.spacing(2)
  }
}));

export default function AverageCalculator() {
  const classes = useStyles();

  const [price1, setPrice1] = React.useState(7500);
  const [price2, setPrice2] = React.useState(7300);
  const [price3, setPrice3] = React.useState(7200);
  const [price4, setPrice4] = React.useState(0);

  const [percentage1, setPercentage1] = React.useState(1);
  const [percentage2, setPercentage2] = React.useState(2);
  const [percentage3, setPercentage3] = React.useState(1);
  const [percentage4, setPercentage4] = React.useState(1);

  const [portfolioUsed, setportfolioUsed] = React.useState(0);
  const [avgPrice, setAvgPrice] = React.useState(0);

  const handleApply = () => {
    const newPortfolioUsed =
      (price1 > 0 ? percentage1 : 0) +
      (price2 > 0 ? percentage2 : 0) +
      (price3 > 0 ? percentage3 : 0) +
      (price4 > 0 ? percentage4 : 0);

    setportfolioUsed(newPortfolioUsed);
    setAvgPrice(
      (price1 * percentage1 +
        price2 * percentage2 +
        price3 * percentage3 +
        price4 * percentage4) /
        newPortfolioUsed
    );
  };

  return (
    <div>
      <form className={classes.root} noValidate autoComplete="off">
        <TextField
          id="price1"
          value={price1}
          label="Primary Buying"
          onChange={(e) => setPrice1(e.target.value)}
        />
        <TextField
          id="perc1"
          select
          label="Portfolio"
          value={percentage1}
          onChange={(e) => setPercentage1(e.target.value)}
        >
          <MenuItem value={0}>0%</MenuItem>
          <MenuItem value={1}>25%</MenuItem>
          <MenuItem value={2}>50%</MenuItem>
          <MenuItem value={3}>75%</MenuItem>
          <MenuItem value={4}>100%</MenuItem>
        </TextField>
      </form>
      <form className={classes.root} noValidate autoComplete="off">
        <TextField
          id="price2"
          value={price2}
          label="Backup 1"
          onChange={(e) => setPrice2(e.target.value)}
        />
        <TextField
          id="perc2"
          select
          label="Portfolio"
          value={percentage2}
          onChange={(e) => setPercentage2(e.target.value)}
        >
          <MenuItem value={0}>0%</MenuItem>
          <MenuItem value={1}>25%</MenuItem>
          <MenuItem value={2}>50%</MenuItem>
          <MenuItem value={3}>75%</MenuItem>
          <MenuItem value={4}>100%</MenuItem>
        </TextField>
      </form>
      <form className={classes.root} noValidate autoComplete="off">
        <TextField
          id="price3"
          value={price3}
          label="Backup 2"
          onChange={(e) => setPrice3(e.target.value)}
        />
        <TextField
          id="perc3"
          select
          label="Portfolio"
          value={percentage3}
          onChange={(e) => setPercentage3(e.target.value)}
        >
          <MenuItem value={0}>0%</MenuItem>
          <MenuItem value={1}>25%</MenuItem>
          <MenuItem value={2}>50%</MenuItem>
          <MenuItem value={3}>75%</MenuItem>
          <MenuItem value={4}>100%</MenuItem>
        </TextField>
      </form>
      <form className={classes.root} noValidate autoComplete="off">
        <TextField
          id="price4"
          value={price4}
          label="Backup 3"
          onChange={(e) => setPrice4(e.target.value)}
        />
        <TextField
          id="perc4"
          select
          label="Portfolio"
          value={percentage4}
          onChange={(e) => setPercentage4(e.target.value)}
        >
          <MenuItem value={0}>0%</MenuItem>
          <MenuItem value={1}>25%</MenuItem>
          <MenuItem value={2}>50%</MenuItem>
          <MenuItem value={3}>75%</MenuItem>
          <MenuItem value={4}>100%</MenuItem>
        </TextField>
      </form>
      <p>
        {portfolioUsed > 4
          ? "Your portfolio percentage selection is wrong"
          : "Average Price: " + avgPrice}
      </p>

      <Button variant="contained" color="primary" onClick={handleApply}>
        Apply
      </Button>
    </div>
  );
}

Also i would recommend you to minimize the number of state variable you have. You could have done something like so

const [prices, setPrices] = useState({});
const [percentages, setPercentages] = useState({});

and the handlers like so

const handleChangePrice = (e) => {
    const { name, value } = e.target;
    setPrices({
      ...prices,
      [name]: value
    });
  };
  const handleChangePercentage = (e) => {
    const { name, value } = e.target;
    setPercentages({
      ...percentages,
      [name]: value
    });
  };

and your Input like so

<TextField
 id="price1"
 value={prices.price1}
 label="Primary Buying"
 onChange={handleChangePrice}
/>

Upvotes: 0

rubendmatos1985
rubendmatos1985

Reputation: 486

check your useEffect function. You are passing an async function but you are getting a race condition because of how useEffect works. The right way to make async operations inside a useEffect function is like this

React.useEffect(()=> {
          const asyncAction = async() => {
            await setportfolioUsed((price1 > 0 ? percentage1 : 0) + (price2 > 0 ? percentage2 : 0) + (price3 > 0 ? percentage3 : 0) + (price4 > 0 ? percentage4 : 0))
            await setAvgPrice((price1 * percentage1 + price2 * percentage2 + price3 * percentage3 + price4 * percentage4) / portfolioUsed)
        }
      
      
        asyncAction();
      }, [update])

If you use a react linter you will se the warning saying this:

'await' has no effect on the type of this expression.ts(80007) Effect callbacks are synchronous to prevent race conditions. Put the async function inside:

Here you have the link with a recreation of your issue: bad use of react useEffect

Upvotes: 1

Related Questions