Reputation: 382
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
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
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