2

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

3 Answers 3

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

Sign up to request clarification or add additional context in comments.

2 Comments

As a side note: I cannot recommend enough using eslint and the react plugin for it. It helps you getting the dependency arrays right, and enforces the correct use of hooks. Both will save you a lot of frustration, especially when starting.
Thank you for your accurate response. This exercise is proposed in a course taken by the University of Helsinki (fullstackopen.com/en/part1/…) in which they ask the student to use this logic of neutral*0 (even if I agree with you this is useless). I'll try your recommendation and I'll let you know, thank you!
3

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.

4 Comments

Yeah, I already wrote a solution trying to separate the variables, but unfortunately it doesn't work as expected.. It seems that average and positive are not updated the first time (in fact, their value is NaN). That's why I ended up coding that "global state" I showed in the question's text.. I'll try to edit my question hoping to give a better explanation of the problem
Maybe this is caused because you are mutation the state instead of setting a new one you should do it as I mentioned above. In your code you are not updating the state you are mutation it. State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions enqueue changes to the component state, but React may delay the changes, updating several components in a single pass.
I've updated my answer I hope now it's clearer
Ok, now I've understood the thing of not directly change the status! I think this is the same approach of the "increment" suggested by @panepeter, who helped my also in updating tot, avg and posPercent, that's why I accepted his answer. However, thank you!
1

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.

1 Comment

Thank you for your answer, however, since I'm starting in these days with React and Javascript, I think your solution is a quite complex for me at the moment. I'll watch it again in a few months hoping to fully understand it!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.