0

As a learning exercise I am trying to code a generic feedback application that counts the number of "good"(+1), "bad"(-1) and "neutral"(0) button clicks, the sum of their values, the average value and positive percentage.

Now I am noticing that the sum –— and only sum! –— value somehow lags one button click behind the events, as the first click, say, on "good" button would produce an average value of "NaN". I assume the sum is somehow still "zero". The rest of the values are being updated as expected.

Could anyone please explain what am I missing here? The App.js code is as follows:

import React, {useState} from 'react'

const Button = (props) => {
  return (
    <>
    <button onClick = {props.handleClick}>{props.text}</button>
  </>
  )
}

const App = () => {
  const [good, setGood] = useState(0)
  const [neutral, setNeutral] = useState(0)
  const [bad, setBad] = useState(0)

  const [all, setAll] = useState(0)
  const [sum, setSum] = useState(0)

  const [average, setAverage] = useState(0)
  const [positive, setPositive] = useState(0)

  const [history, setHistory] = useState([])

  const handleGoodClick = () => {
    setAll(all + 1)
    setSum(sum + 1)
    setGood(good + 1)

    setHistory(history.concat(1))
    setPositive(good / all)
    setAverage(sum / all)

  }

  const handleBadClick = () => {
    setAll(all + 1)
    setSum(sum - 1)
    setBad(bad + 1)

    setHistory(history.concat(-1))
    setPositive(good / all)
    setAverage(sum / all)
  }

  const handleNeutralClick = () => {
    setAll(all + 1)
    setNeutral(neutral + 1)

    setHistory(history.concat(0))
    setPositive(good / all)
    setAverage(sum / all)

  }

  return (
    <>
      <h2>Give Feedback</h2>
      <Button handleClick = {handleGoodClick} text = {'good'}/>
      <Button handleClick = {handleNeutralClick} text = {'neutral'}/>
      <Button handleClick = {handleBadClick} text = {'bad'}/>

      <h2>Statistics</h2>
      <p>Good: {good}</p>
      <p>Neutral: {neutral}</p>
      <p>Bad: {bad}</p>
      <p>All: {all}</p>
      <p>Average: {average}</p>
      <p>Positive: {positive}</p>
      <p>History: {history.join(' ')}</p>
    </>
  )
}

export default App
qlep
  • 15
  • 5
  • As a side note, I'd say it does not make a lot of sense to me keeping in the state the values for `sum`, `all`, `average` and `positive` as they are computed from other state variables (`good`, `neutral`, and `bad`). – secan Apr 20 '21 at 12:56
  • to use previous state I would try to use functional set, for example: `setAll((all) => all + 1)` – rustyBucketBay Apr 20 '21 at 12:57
  • yep, understood. Those are there because I have totally forgotten about them. – qlep Apr 20 '21 at 12:59
  • `sum` and `all` can be derived from the state you already have. `const sum = good + neutral + bad` This way you avoid bugs related to state being out of sync. Your issue is probably because state updates are batched together and at the moment you update average, sum is not yet updated. – Oskar Apr 20 '21 at 13:00
  • @rustyBucketBay eeeh, elaborate? – qlep Apr 20 '21 at 13:01
  • @Oskar can you suggest a solution? This is a beginners exercise and I can only assume that displaying an average value should be fairly straightforward – qlep Apr 20 '21 at 13:03
  • @Oskar, I get the sum, but how would I go about deriving the number of clicks from it? – qlep Apr 20 '21 at 13:06
  • @qlep keep in state only `good, neutral, bad` derive other variables in function body like so: `const all = good + neutral + bad; const positive = good / all; const sum = good - bad;` – Oskar Apr 20 '21 at 13:10
  • so in `handleGoodClick` you would have only `setGood(good + 1)` – Oskar Apr 20 '21 at 13:12
  • https://www.youtube.com/watch?v=O6P86uwfdR0 min 6:35 – rustyBucketBay Apr 20 '21 at 13:13
  • not sure if that is the source of your problem as I did not dig into it, but just in case, when I use previous states I tend to use this functional approach – rustyBucketBay Apr 20 '21 at 13:15
  • @Oskar, seems to me "positive" and "average" should be calculated in functions, as the value of "all" is zero by default – qlep Apr 20 '21 at 13:28
  • @secan, your side note gave the beginning to the solution. Thanks! – qlep Apr 20 '21 at 13:47
  • The main issue here is that `setAll(all + 1)` does not update `all` in the local scope. (The state change is queued and the new value is available in the next render.) So when you try to use the new `all` value in for example `setPositive(good / all)` it is using a value that lacks one behind. The same applies for other states that you expect to be updated. See: [useState set method not reflecting change immediately](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) – 3limin4t0r Apr 20 '21 at 13:56
  • @qlep, I am happy I could help. Potentially, you could manage everything with a single state variable (your `history`, which in my example I called `votes`) and calculate all the others data; you can see an example here: https://codesandbox.io/s/cool-solomon-d4yfi – secan Apr 20 '21 at 15:46
  • @3limin4t0r, thank you for the input. I will dig into the topic. – qlep Apr 21 '21 at 16:27

1 Answers1

1

Try to refactor into something like this:

  const [good, setGood] = useState(0)
  const [neutral, setNeutral] = useState(0)
  const [bad, setBad] = useState(0)

  const handleGoodClick = () => {
    setGood(good + 1)
  }

  const handleBadClick = () => {
    setBad(bad + 1)
  }

  const handleNeutralClick = () => {
    setNeutral(neutral + 1)
  }

  const all = good + bad + neutral;
  const positive = good / all;
  const sum = good - bad;
  const average = sum / all;

read this article for more details: https://kentcdodds.com/blog/dont-sync-state-derive-it

Oskar
  • 135
  • 1
  • 12
  • 1
    Thanks, @Oskar! After wrapping `positive` and `average` calculations in functions I got the whole thing working properly. – qlep Apr 20 '21 at 13:45