React Hooks 如何处理多个状态的并发更新?

发布于 2025-01-16 20:06:01 字数 2037 浏览 0 评论 0原文

在下面的代码中,我通过使用唯一的全局“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 "complex 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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

红衣飘飘貌似仙 2025-01-23 20:06:01

useMemo,请

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

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

useMemo 每当给定之一时重新计算一个值依赖关系更改:

const total = useMemo(() => good + neutral + bad), [good, neutral, bad]);

对所有三个计算值都进行此更改后,您只需负责更新良好、中立和不良计数。

功能更新

请注意如何使用功能更新 使您的处理程序非常精简:

// … 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 不过看起来有点多余。如果我的数学没有让我失败,你可以把它忽略掉。

useMemo, 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 component
const increment = (x) => x + 1;

// Then in your component:
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.

小猫一只 2025-01-23 20:06:01

状态不应该被改变,因为这可能会导致错误和奇怪的行为。如果您需要根据当前值更新状态,您可以这样做:

const [state, setState] = useState(1);

const updateStateHandler = () => {
    setState(prevState => setState + 1);
}

这样您就可以使用以前的状态来设置新状态。

在您的代码中,我认为第二种方法可能更好,每个属性都有单独的状态,如果您希望将其全部放在一个状态中,您可以看看 减速器挂钩

在您的情况下,handleGood 函数应该是:

const handleGood = () => {
    setGood(prevState => prevState + 1);
    setTot(prevState => prevState + 1);
    setAvg((good*1 + neutral*0 + bad*(-1))/tot);
    setPosPercent((good*100)/tot);
}

如果您使用先前的值来更新状态,则必须传递一个接收先前值并返回新值的函数。

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.

失与倦" 2025-01-23 20:06:01

该解决方案旨在提供一个基于 OP 与 useMemo 结合的堆栈片段答案,并使其更加健壮(如果需要添加新选项,请说“非常好”)或“非常糟糕”)。

代码片段

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>

注意

请使用整页查看演示 - 这样更容易。

说明

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

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

NOTE

Please use Full Page to view the demo - it's easier that way.

Explanation

There are inline comments in the above snippet for reference.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文