Consequences of using computed variables vs useState/useEffect

Viewed 653

If I have a variable whose value can be fully derived based on the value of another property, is there any consequence/pitfall to initializing a computed variable vs using a combination of useState/useEffect to track the variable? Let me illustrate with a contrived example:

/**
 * ex paymentAmounts: [100, 300, 400]
 */
const Option1 = ({paymentAmounts}) => {
  const [average, setAverage] = useState(paymentAmounts.reduce((acc, curr) => curr + acc, 0) / paymentAmounts.length)

  useEffect(() => {
    setAverage(paymentAmounts.reduce((acc, curr) => curr + acc, 0) / paymentAmounts.length)
  }, [paymentAmounts])

  return (
    <div>
      Average: {average}
    </div>
  )
}

or more simply

/**
 * ex paymentAmounts: [100, 300, 400]
 */
const Option2 = ({paymentAmounts}) => {
  const average = paymentAmounts.reduce((acc, curr) => curr + acc, 0) / paymentAmounts.length

  return (
    <div>
      Average: {average}
    </div>
  )
}

Am I giving up any control and/or React benefits by using Option2?

Vue.js seems to have this option via computed properties.

3 Answers

The only reason to use hooks or other state tracking functionality is if you plan on changing the state within the component itself. From the example you gave, that is not the case. If the prop paymentAmounts is updated the component will get updated by React and so will your computed constant, average.

So, you get no benefit from using useState & useEffect here. Keep it simple with your computed constant!

You use state (as in useState that is) when you want to track something in your component (or share it between its children) between rerenders. In your case, you're getting this "state" from your parent component (the parent is the actual state holder). Every change in the parent state (paymentAmounts in your situation) will reflect in your child component automatically.

As a general "rule", don't use state for data that can be calculated. Keep in mind that every change of a state tracked variable will force the component to re-render. Another bad usage example is this:

const Example = ({variable1}) => {
  const [variable, setVariable] = useState(variable1);

  ...
}

A couple of additional notes:

  1. In your first solution you introduce an additional overhead by using useEffect. As pointed out already, your child component will always re-render and recalculate the average when your parent's state change.
  2. You're using setAverage wrong in your useEffect hook. The setter takes either a new value or a function that accepts the current one and returns the new one.
  3. Not sure what you expect from the paymentAmounts / paymentAmounts.length calculation. I suppose it is just a dummy code, but if not, look into it. You're dividing the array itself (not the sum of its values) by its length.

So, in short - Yes, you should be using a simple variable to calculate the average and ditch the useState/useEffect. Not only you're NOT giving up any benefits, you're actually making your code more performant, maintainable, readable and error-free.

Let's analyze the Option1 component step-by-step:

/**
 * ex paymentAmounts: [100, 300, 400]
 */
const Option1 = ({paymentAmounts}) => {

  //1
  const [average, setAverage] = useState(paymentAmounts / paymentAmounts.length)

  //2
  useEffect(() => {
   setAverage(paymentAmounts.reduce((acc, curr) => curr + acc, 0) / paymentAmounts.length)
  }, [ paymentAmounts])

  return (
    <div>
      Average: {average}
    </div>
  )
}
  1. Here you are initializing the average state variable from the paymentAmounts prop. During the initial render, the initial value of the average state variable would be calculated from it. So far so good.
  2. In the useEffect you are adding a dependency on the paymentAmounts array from the prop. The effect callback will run when a new array instance is passed as a prop and also on the initial render. So here is the catch, the effect callback won't run when you push new numbers in the existing array reference, it will only run when you pass a new array instance in the prop having the updated numbers. So you may see no change in the average if you simply do paymentAmounts.push(400) in Option1's parent component.

So for all use cases Option2 is the best way to achieve what you wanted to do. There is no need to introduce a state in this functional component. The value in the UI can be derived from the props itself.

Related