React Hooks 如何处理多个状态的并发更新?
How to handle concurrent update of multiple states in React Hooks?
我是 React 的新手,我正在尝试自学如何在 React Hooks 中实现前端。
在下面的代码中,我使用唯一的全局“状态”处理了多个状态变量的并发更改,但我认为这不是最好的方法。
有人可以建议我如何更改多个状态而不像我那样将它们放在一起吗?
提前致谢!
这是具有“复杂状态”的工作代码
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
[已编辑]
这是@panepeter
提出的解决方案的工作代码
import Button from './Button';
import { useState, useMemo } from 'react'
const increment = (x) => x + 1;
const App = () => {
const [good, setGood] = useState(0);
const [neutral, setNeutral] = useState(0);
const [bad, setBad] = useState(0);
const tot = useMemo(() => computeTot(), [good, neutral, bad]);
const avg = useMemo(() => computeAvg(), [good, neutral, bad]);
const posPercent = useMemo(() => computePosPercent(), [good, neutral, bad]);
function computeTot() {
if (good === 0 && bad === 0 && neutral === 0) {
return 0;
}
else {
return (good + neutral + bad);
}
}
function computeAvg() {
if (good === 0 && bad === 0 && neutral === 0) {
return 0;
}
else {
return ((good*1 + neutral*0 + bad*(-1))/tot);
}
}
function computePosPercent() {
if (good === 0 && bad === 0 && neutral === 0) {
return 0;
}
else {
return ((good*100)/tot);
}
}
const handleGood = () => setGood(increment);
const handleNeutral = () => setNeutral(increment);
const handleBad = () => setBad(increment);
return (
<div>
<h1>give feedback</h1>
<Button onClick={handleGood} text="good" />
<Button onClick={handleNeutral} text="neutral" />
<Button onClick={handleBad} text="bad" />
<h1>statistics</h1>
<p>good {good}</p>
<p>neutral {neutral}</p>
<p>bad {bad}</p>
<p>all {tot}</p>
<p>average {avg}</p>
<p>positive {posPercent} %</p>
</div>
)
}
export default App
不应改变状态,因为这可能会导致错误和奇怪的行为。如果你需要根据当前值更新你的状态,你可以这样做:
const [state, setState] = useState(1);
const updateStateHandler = () => {
setState(prevState => setState + 1);
}
这样您就可以使用以前的状态设置新状态。
在你的代码中,我认为第二种方法可能更好,每个属性都有单独的状态,如果你想把它们放在一个状态中,你可以看看 reducer hook。
在你的情况下,handleGood
函数应该是:
const handleGood = () => {
setGood(prevState => prevState + 1);
setTot(prevState => prevState + 1);
setAvg((good*1 + neutral*0 + bad*(-1))/tot);
setPosPercent((good*100)/tot);
}
如果您使用以前的值来更新状态,则必须传递一个接收以前的值和 returns 新值的函数。
useMemo
,请
我在这里看到的最大问题(查看您的第二段代码)是您手动尝试更新计算出的值(即 posPercent
、avg
、tot
)
这当然可行,但比您可能想要的要麻烦得多。
useMemo
re-calculates 每当给定依赖项之一发生更改时的值:
const total = useMemo(() => good + neutral + bad), [good, neutral, bad]);
有了这三个计算值,您只需负责更新良好、中性、不良计数。
功能更新
请注意如何使用 functional updates
使您的处理程序非常精简:
// … this could/should be defined outside of the component
const increment = (x) => x + 1;
// Then in your component:
const handleGood = setGood(increment)
const handleBad = setGood(increment)
// …
这只是一种风格选择,setGood(good + 1)
也可以。我喜欢它,因为 increment
非常易读。
和一点数学知识
老实说,我没有更深入地了解您要计算的内容。 neutral*0
虽然看起来有点多余。如果我的数学没让我失望,你可以把它去掉。
该解决方案旨在根据 OP 结合 useMemo
的答案提供 stack-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 compute 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.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
注意
请使用 Full Page
观看演示 - 这样更容易。
说明
上面的代码片段中有内嵌注释以供参考。
我是 React 的新手,我正在尝试自学如何在 React Hooks 中实现前端。
在下面的代码中,我使用唯一的全局“状态”处理了多个状态变量的并发更改,但我认为这不是最好的方法。
有人可以建议我如何更改多个状态而不像我那样将它们放在一起吗? 提前致谢!
这是具有“复杂状态”的工作代码
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
[已编辑]
这是@panepeter
提出的解决方案的工作代码import Button from './Button';
import { useState, useMemo } from 'react'
const increment = (x) => x + 1;
const App = () => {
const [good, setGood] = useState(0);
const [neutral, setNeutral] = useState(0);
const [bad, setBad] = useState(0);
const tot = useMemo(() => computeTot(), [good, neutral, bad]);
const avg = useMemo(() => computeAvg(), [good, neutral, bad]);
const posPercent = useMemo(() => computePosPercent(), [good, neutral, bad]);
function computeTot() {
if (good === 0 && bad === 0 && neutral === 0) {
return 0;
}
else {
return (good + neutral + bad);
}
}
function computeAvg() {
if (good === 0 && bad === 0 && neutral === 0) {
return 0;
}
else {
return ((good*1 + neutral*0 + bad*(-1))/tot);
}
}
function computePosPercent() {
if (good === 0 && bad === 0 && neutral === 0) {
return 0;
}
else {
return ((good*100)/tot);
}
}
const handleGood = () => setGood(increment);
const handleNeutral = () => setNeutral(increment);
const handleBad = () => setBad(increment);
return (
<div>
<h1>give feedback</h1>
<Button onClick={handleGood} text="good" />
<Button onClick={handleNeutral} text="neutral" />
<Button onClick={handleBad} text="bad" />
<h1>statistics</h1>
<p>good {good}</p>
<p>neutral {neutral}</p>
<p>bad {bad}</p>
<p>all {tot}</p>
<p>average {avg}</p>
<p>positive {posPercent} %</p>
</div>
)
}
export default App
不应改变状态,因为这可能会导致错误和奇怪的行为。如果你需要根据当前值更新你的状态,你可以这样做:
const [state, setState] = useState(1);
const updateStateHandler = () => {
setState(prevState => setState + 1);
}
这样您就可以使用以前的状态设置新状态。
在你的代码中,我认为第二种方法可能更好,每个属性都有单独的状态,如果你想把它们放在一个状态中,你可以看看 reducer hook。
在你的情况下,handleGood
函数应该是:
const handleGood = () => {
setGood(prevState => prevState + 1);
setTot(prevState => prevState + 1);
setAvg((good*1 + neutral*0 + bad*(-1))/tot);
setPosPercent((good*100)/tot);
}
如果您使用以前的值来更新状态,则必须传递一个接收以前的值和 returns 新值的函数。
useMemo
,请
我在这里看到的最大问题(查看您的第二段代码)是您手动尝试更新计算出的值(即 posPercent
、avg
、tot
)
这当然可行,但比您可能想要的要麻烦得多。
useMemo
re-calculates 每当给定依赖项之一发生更改时的值:
const total = useMemo(() => good + neutral + bad), [good, neutral, bad]);
有了这三个计算值,您只需负责更新良好、中性、不良计数。
功能更新
请注意如何使用 functional updates
使您的处理程序非常精简:
// … this could/should be defined outside of the component
const increment = (x) => x + 1;
// Then in your component:
const handleGood = setGood(increment)
const handleBad = setGood(increment)
// …
这只是一种风格选择,setGood(good + 1)
也可以。我喜欢它,因为 increment
非常易读。
和一点数学知识
老实说,我没有更深入地了解您要计算的内容。 neutral*0
虽然看起来有点多余。如果我的数学没让我失望,你可以把它去掉。
该解决方案旨在根据 OP 结合 useMemo
的答案提供 stack-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 compute 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.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
注意
请使用 Full Page
观看演示 - 这样更容易。
说明
上面的代码片段中有内嵌注释以供参考。