简体   繁体   中英

How to handle concurrent update of multiple states in React Hooks?

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

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM