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,请

我在这里看到的最大问题(查看您的第二段代码)是您手动尝试更新计算出的值(即 posPercentavgtot)

这当然可行,但比您可能想要的要麻烦得多。

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 观看演示 ​​- 这样更容易。

说明

上面的代码片段中有内嵌注释以供参考。