In the code below, I've handled the concurrent change of multiple state variables by using a unique global "State", but I don't think it is the best way to do so.
Can anybody suggest me how to change multiple states without keeping them together as I did?
Here's the working code with the "plex state"
import { useState } from 'react'
const App = () => {
const [state, setState] = useState({
good: 0,
neutral: 0,
bad: 0,
tot: 0,
weights: 0,
avg: 0,
posPercent: 0
});
const handleGood = () => {
setState({
...state,
good: state.good +1,
tot: state.tot +1,
weights: (state.good+1)*1 + state.neutral*0 + state.bad*(-1),
avg: ((state.good+1)*1 + state.neutral*0 + state.bad*(-1))/(state.tot +1),
posPercent: ((state.good+1)*100)/(state.tot+1)
});
}
const handleNeutral = () => {
setState({
...state,
neutral: state.neutral +1,
tot: state.tot +1,
weights: state.good*1 + (state.neutral+1)*0 + state.bad*(-1),
avg: (state.good*1 + (state.neutral+1)*0 + state.bad*(-1))/(state.tot +1),
posPercent: ((state.good)*100)/(state.tot+1)
});
}
const handleBad = () => {
setState({
...state,
bad: state.bad +1,
tot: state.tot +1,
weights: state.good*1 + state.neutral*0 + (state.bad+1)*(-1),
avg: (state.good*1 + state.neutral*0 + (state.bad+1)*(-1))/(state.tot +1),
posPercent: ((state.good)*100)/(state.tot+1)
});
}
return (
<div>
<h1>give feedback</h1>
<button onClick={handleGood}>
good
</button>
<button onClick={handleNeutral}>
neutral
</button>
<button onClick={handleBad}>
bad
</button>
<h1>statistics</h1>
<p>good {state.good}</p>
<p>neutral {state.neutral}</p>
<p>bad {state.bad}</p>
<p>all {state.tot}</p>
<p>average {state.avg}</p>
<p>positive {state.posPercent} %</p>
</div>
)
}
export default App
In the code below, I've handled the concurrent change of multiple state variables by using a unique global "State", but I don't think it is the best way to do so.
Can anybody suggest me how to change multiple states without keeping them together as I did?
Here's the working code with the "plex state"
import { useState } from 'react'
const App = () => {
const [state, setState] = useState({
good: 0,
neutral: 0,
bad: 0,
tot: 0,
weights: 0,
avg: 0,
posPercent: 0
});
const handleGood = () => {
setState({
...state,
good: state.good +1,
tot: state.tot +1,
weights: (state.good+1)*1 + state.neutral*0 + state.bad*(-1),
avg: ((state.good+1)*1 + state.neutral*0 + state.bad*(-1))/(state.tot +1),
posPercent: ((state.good+1)*100)/(state.tot+1)
});
}
const handleNeutral = () => {
setState({
...state,
neutral: state.neutral +1,
tot: state.tot +1,
weights: state.good*1 + (state.neutral+1)*0 + state.bad*(-1),
avg: (state.good*1 + (state.neutral+1)*0 + state.bad*(-1))/(state.tot +1),
posPercent: ((state.good)*100)/(state.tot+1)
});
}
const handleBad = () => {
setState({
...state,
bad: state.bad +1,
tot: state.tot +1,
weights: state.good*1 + state.neutral*0 + (state.bad+1)*(-1),
avg: (state.good*1 + state.neutral*0 + (state.bad+1)*(-1))/(state.tot +1),
posPercent: ((state.good)*100)/(state.tot+1)
});
}
return (
<div>
<h1>give feedback</h1>
<button onClick={handleGood}>
good
</button>
<button onClick={handleNeutral}>
neutral
</button>
<button onClick={handleBad}>
bad
</button>
<h1>statistics</h1>
<p>good {state.good}</p>
<p>neutral {state.neutral}</p>
<p>bad {state.bad}</p>
<p>all {state.tot}</p>
<p>average {state.avg}</p>
<p>positive {state.posPercent} %</p>
</div>
)
}
export default App
Share
Improve this question
edited Nov 19, 2022 at 12:18
Kalle Richter
8,78729 gold badges93 silver badges207 bronze badges
asked Mar 25, 2022 at 11:21
LauGroupie95LauGroupie95
471 silver badge7 bronze badges
3 Answers
Reset to default 3useMemo
, please
The biggest issue I see here (looking at your 2nd piece of code), is that you're manually trying to update values that are calculated (namely, posPercent
, avg
, tot
)
That's certainly doable, but it's a lot more headache than you probably want.
useMemo
re-calculates a value whenever one of the given dependencies changes:
const total = useMemo(() => good + neutral + bad), [good, neutral, bad]);
With this in place for all three calculated values, you're only responsible for updating the good, neutral, bad counts.
Functional updates
Note how you can use functional updates
to make your handlers very streamlined:
// … this could/should be defined outside of the ponent
const increment = (x) => x + 1;
// Then in your ponent:
const handleGood = setGood(increment)
const handleBad = setGood(increment)
// …
This is merely a stylistic choice, setGood(good + 1)
works just as well. I like it because increment
is so nicely readable.
and a bit of math
I honestly didn't get any deeper into what you're trying to calculate. neutral*0
though seems, well, a bit redundant. If my math doesn't fail me here, you could just leave this out.
States shouldn't be mutated because that may lead you to bugs and strange behaviours. If you need to update your state based on the current value you can do it like this:
const [state, setState] = useState(1);
const updateStateHandler = () => {
setState(prevState => setState + 1);
}
This way you can use your previous state to set a new state.
In your code I think maybe it's better the second approach with individual states for every attribute and if you want it all together in one state you may take a look at reducer hook.
In your case the handleGood
function shoudl be:
const handleGood = () => {
setGood(prevState => prevState + 1);
setTot(prevState => prevState + 1);
setAvg((good*1 + neutral*0 + bad*(-1))/tot);
setPosPercent((good*100)/tot);
}
If you use the previous value to update state, you must pass a function that receives the previous value and returns the new value.
This solution seeks to provide a stack-snippet answer based on the one by OP in conjunction with useMemo
as well as make it a tad more robust (if one needs to add new options, say "very good" or "very bad").
Code Snippet
const {useState, useMemo} = React;
const App = () => {
const increment = (x) => x + 1;
// below array drives the rendering and state-creation
const fbOptions = ['good', 'neutral', 'bad'];
// any new options added will automatically be included to state
const initState = fbOptions.reduce(
(acc, op) => ({
...acc,
[op]: 0
}),
{}
);
const [options, setOptions] = useState({...initState});
// calculate total when options change
const tot = useMemo(() => (
Object.values(options).reduce(
(tot, val) => tot + +val,
0
)
), [options]);
// helper methods to calculate average, positive-percentage
// suppose one changes from good-neutral-bad to a star-rating (1 star to 5 stars)
// simply tweak the below methods to modify how average + pos-percent are calculated.
const getAvg = (k, v) => (
v * ( k === 'good' ? 1 : k === 'bad' ? -1 : 0 )
);
const getPosPercent = (k, v, tot, curr) => (
k === 'good' ? (v * 100) / tot : curr
);
// unified method to pute both avg and posPercent at once
const {avg = 0, posPercent = 0} = useMemo(() => (
tot &&
Object.entries(options).reduce(
(acc, [k, v]) => ({
avg: acc.avg + getAvg(k, v)/tot,
posPercent: getPosPercent(k, v, tot, acc.posPercent)
}),
{avg: 0.0, posPercent: 0.0}
)
), [options]);
// the UI rendered below is run from template 'options' array
// thus, no changes will be needed if we modify 'options' in future
return (
<div>
<h4>Give Feedback</h4>
{
fbOptions.map(op => (
<button
key={op}
id={op}
onClick={() => setOptions(
prev => ({
...prev,
[op]: increment(prev[op])
})
)}
>
{op}
</button>
))
}
<h4>Statistics</h4>
{
fbOptions.map(op => (
<p>{op} : {options[op]}</p>
))
}
<p>all {tot}</p>
<p>average {avg.toFixed(2)}</p>
<p>positive {posPercent.toFixed(2)} %</p>
</div>
)
};
ReactDOM.render(
<div>
<h3>DEMO</h3>
<App />
</div>,
document.getElementById("rd")
);
h4 { text-decoration: underline; }
button {
text-transform: uppercase;
padding: 5px;
border-radius: 7px;
margin: 5px 10px;
border: 2px solid lightgrey;
cursor: pointer;
}
<div id="rd" />
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
NOTE
Please use Full Page
to view the demo - it's easier that way.
Explanation
There are inline ments in the above snippet for reference.