🛠️ Calculator: State

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Calculator: State

Custom attributes (saved inside dataset) are converted into strings and appear in the HTML automatically.

We saved some values inside custom attributes with dataset. In this case, we saved firstValue, operator, modifierValue and previousButtonType in calculatorElement.dataset.

Here’s an example:

export default function Calculator () {
  // Declare Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      calculatorElement.dataset.previousButtonType = buttonType
    }
  }

  // Add event listeners
  // Return Calculator
}

Since we use firstValue, operator, modifierValue and previousButtonType inside JavaScript directly, there’s no need to pass these information back to HTML. We can use a normal JavaScript object instead.

When we use a JavaScript object to store information about a component, we often call this object state.

Switching from dataset to state

First, we need to create a state variable to hold information. We will begin with an empty object.

export default function Calculator () {
  // ...
  const state = {}
  const calculator = {/* ... */}

  // Add event listeners
  // Return calculator
}

Next, we go through the code and convert all calculatorElement.dataset values into state. Here, you can do a find and replace.

I like to add all state properties into the initial state declaration. This tells me what properties I can expect to see inside the state object.

In this case, we have four properties:

  1. operator
  2. firstValue
  3. modifierValue
  4. previousButtonType

Since we begin the calculator with all of these values unset, the correct value for each property should be undefined.

export default function Calculator () {
  // ...
  const state = {
    operator: undefined,
    firstValue: undefined,
    modifierValue: undefined,
    previousButtonType: undefined
  }

  const calculator = {/* ... */}

  // Add event listeners
  // Return calculator
}

Although undefined is the actual value, I tend to initialise state properties to either an empty string (''). I do this because it’s much easier to browse through the state object.

export default function Calculator () {
  // ...
  const state = {
    operator: '',
    firstValue: '',
    modifierValue: '',
    previousButtonType: ''
  }

  const calculator = {/* ... */}

  // Add event listeners
  // Return calculator
}

At this point, you’ll run into some errors.

There are two kinds of errors:

  1. Errors related to Clearing After Calculation
  2. Errors related to the “Number Operator Equal Equal” calculation

Fixing “Clearing After Calculation” errors

If you look at testClearAfterCalculation in calculator.test.js, you’ll notice we still use dataset in the test. We need to switch it to state.

Before we can use state in the test file, we need to expose the state via a getter function. This getter function ensures state value retrieved is always up to date.

Another benefit: Users cannot overwrite the state object (and hence mess things up) since there is no setter function.

export default function Calculator () {
  // Declare Variables
  const state = {/* ... */}
  const calculator = {
    get state () { return state },
    // Other methods
  }
}

We can now get the calculator’s state like this:

function testClearAfterCalculation () {
  // ...
  const { firstValue, operator } = calculator.state
  // ...
}

Tests related to “Clearing After Calculation” should pass now.

Removing false positives

If you look at testFullClear, you’ll see we’re still using dataset as well, but the test passes!

// We are still using `dataset` in `testFullClear`
function testFullClear () {
  // ...
  console.assert(!calculator.element.dataset.firstValue, 'Full Clear:No first value')
  console.assert(!calculator.element.dataset.operator, 'Full Clear: No operator value')
  console.assert(!calculator.element.dataset.modifierValue, 'Full Clear: No operator value')
}

We need to change dataset to state here as well.

function testFullClear () {
  // ...
  console.assert(!calculator.state.firstValue, 'Full Clear:No first value')
  console.assert(!calculator.state.operator, 'Full Clear: No operator value')
  console.assert(!calculator.state.modifierValue, 'Full Clear: No operator value')
}

Fixing the calculation error

When we switched from dataset to state, we broke one test case: Number -> Operator -> Equal -> Equal. But why?

One of the things I did when debugging this was to log firstValue into the console. When I did this, I noticed firstValue was a String on most occasions, but it changed to a Number on other occasions. This is evident from the purple text (which denotes numbers) in Chrome Devtools.

But why is firstValue, which usually is a String, become a Number?

Turns out, firstValue is a String because we get that value from the HTML (which can only return strings). However, when we calculate the result, we saved the result back into state.firstValue. This result is a Number.

export default function Calculator () {
  // ...
  const calculator = {
    handleEqualKey () {
      // ...
      if (firstValue && operator) {
        const result = calculator.calculate(firstValue, operator, secondValue)
        calculator.displayValue = result
        state.firstValue = result // Saves result into state. This result is a number.
        state.modifierValue = secondValue
      } else {
        calculator.displayValue = parseFloat(displayValue) * 1
      }
    },

    // ...
  }

  // ...
}

It’s confusing when we deal with two data types at the same time. In this case, I recommend we stick to Strings because the dataset version uses Strings.

We can convert Numbers back into Strings easily with the toString method. We’ll do this before we return the calculated value.

export default function Calculator () {
  // Declare Variables

  const calculator = {
    calculate (firstValue, operator, secondValue) {
      firstValue = parseFloat(firstValue)
      secondValue = parseFloat(secondValue)

      let result
      if (operator === 'plus') result = firstValue + secondValue
      if (operator === 'minus') result = firstValue - secondValue
      if (operator === 'times') result = firstValue * secondValue
      if (operator === 'divide') result = firstValue / secondValue

      return result.toString()
    },

    // Other methods
  }

  // Add event listeners
  // Return calculator
}

Note: We should not use multiple if statements if we’re not doing early returns. We should use switch instead, because it’s clear that code doesn’t flow into unnecessary if conditions.

export default function Calculator () {
  // Declare Variables

  const calculator = {
    calculate (firstValue, operator, secondValue) {
      // ...

      let result
      switch (operator) {
        case 'plus': result = firstValue + secondValue; break
        case 'minus': result = firstValue - secondValue; break
        case 'times': result = firstValue * secondValue; break
        case 'divide': result = firstValue / secondValue; break
      }

      return result.toString()
    },

    // Other methods
  }

  // Add event listeners
  // Return calculator
}

This fixes the error.

And that’s all we have to do to use the state variable instead of dataset.

Bonus

Take a look at handleClick. Did you notice we had to pass button into some handler methods?

export default function Calculator () {
  // Declare Variables

  const calculator = {
    handleClick () {
      // ...
      switch (buttonType) {
        case 'clear': calculator.handleClearKey(); break
        case 'number': calculator.handleNumberKeys(button); break
			  case 'decimal': calculator.handleDecimalKey(); break
			  case 'operator': calculator.handleOperatorKeys(button); break
			  case 'equal': calculator.handleEqualKey(); break
			}
		},

		// Other methods
	}

  // Add event listeners
  // Return calculator
}

Is there a way to standardise the functions so we don’t pass button into some of them? I’ll leave you to figure this out as a bonus :)