最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to handle concurrent update of multiple states in React Hooks? - Stack Overflow

programmeradmin3浏览0评论

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
Add a ment  | 

3 Answers 3

Reset to default 3

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

发布评论

评论列表(0)

  1. 暂无评论