Combining multiple states
→ Click here to see the state of the app after completing this step.
→ Click here to see the changes we'll make in this step in GitHub.
Looking back, a crucial step in order to make our calculator functional was to introduce dependencies between set-state-handlers and other state variables. Now we have three state variables, and there are already multiple dependencies we have to keep track of in order to understand how the calculator works.
It would be a lot nicer to just have one state variable with one set-state-handler that does everything. And we will now see how we can achieve exactly that using gg!
State in gg does not need to be a primitive value like strings or numbers. It can be anything. Let's introduce a new state variable that will replace the existng three ones.
3const firstNumber = gg.state(""); 4const operator = gg.state(""); 5const secondNumber = gg.state("");+6const calculatorState = gg.state({+7 firstNumber: "",+8 operator: "",+9 secondNumber: "",+10}); 11
Now we want to use this state variable in the display of the calculator. However, if we can't just use the state variable in the JSX expression because calculatorState does not have a primitive value. Let's see what happens anyway.
165 <div class={[stylesheet.getClass("display")]}>-{firstNumber} {operator} {secondNumber}+166 {calculatorState} 167 </div>
What we now see is the display is more or less what we would expect when you stringify an object: [object Object]. We have to somehow tell the state how we want to stringify it. In gg we can do this using selectors.
We can add selectors when defining the state variable by passing an object as second argument to gg.state. The keys of the object are the names you can choose for the selector. The value has to be a function that will be called with the current state value and has to return a primitive value.
For now we just need one selector, which will be used to show the whole calculation in the calculator display.
5const secondNumber = gg.state("");-const calculatorState = gg.state({-firstNumber: "",-operator: "",-secondNumber: "",-});+6const calculatorState = gg.state(+7 {+8 firstNumber: "",+9 operator: "",+10 secondNumber: "",+11 },+12 {+13 display: (value) =>+14 [value.firstNumber, value.operator, value.secondNumber].join(" "),+15 },+16); 17
Now we can access our display selector using the selectors property of state variable.
172 <div class={[stylesheet.getClass("display")]}>-{calculatorState}+173 {calculatorState.selectors.display} 174 </div>
We will also need a set-state-handler for the calculatorState. As always, we derive the clicked button from the event target.
86 +87const setCalculatorState = gg.setState(+88 calculatorState,+89 (value, event) => {+90 if (!(event.target instanceof HTMLButtonElement)) {+91 return value;+92 }+93 const buttonText = event.target.innerText;+94 return value;+95 },+96 [],+97);+98 99const buttonStyles = gg.stylesheet(`
Now we have to unify the functionality that we previously implemented for the three separated states. We'll use a switch statement to handle the different buttons. For the digits from one to nine we want to add it to either the first number (if there is no operator yet) or the second number (if there already is an operator).
93 const buttonText = event.target.innerText;+94 switch (buttonText) {+95 case "1":+96 case "2":+97 case "3":+98 case "4":+99 case "5":+100 case "6":+101 case "7":+102 case "8":+103 case "9": {+104 const number = value.operator ? "secondNumber" : "firstNumber";+105 value[number] += buttonText;+106 break;+107 }+108 } 109 return value;
The zero-digit and the decimal point behave similar, but we also need to remember the special cases we implemented in the reducers for both numbers.
107 }+108 case "0": {+109 const number = value.operator ? "secondNumber" : "firstNumber";+110 if (value[number]) {+111 value[number] += buttonText;+112 }+113 break;+114 }+115 case ".": {+116 const number = value.operator ? "secondNumber" : "firstNumber";+117 if (!value[number].includes(".")) {+118 value[number] += buttonText;+119 }+120 break;+121 } 122 }
At last, let's also handle updating the operator. This should again only be allowed when there already is a first number but not yet a second number.
121 }+122 case "+":+123 case "-":+124 case "×":+125 case "÷": {+126 if (+127 value.firstNumber &&+128 value.firstNumber !== "." &&+129 !value.secondNumber+130 ) {+131 value.operator = buttonText;+132 }+133 break;+134 } 135 }
Now that we reimplemented all the functionality, we can pass the new set-state-handler as onclick attribute to our buttons instead of the three handlers for the individual states.
184 return (-<button-class={classes}-onclick={[setFirstNumber, setOperator, setSecondNumber]}->+185 <button class={classes} onclick={setCalculatorState}> 186 {args.children} 187 </button> 188 );
At last, let's clean up and remove the three individual states as well as their set-state-handlers.
2 -const firstNumber = gg.state("");-const operator = gg.state("");-const secondNumber = gg.state("");3const calculatorState = gg.state( ⋮ 14 -const setFirstNumber = gg.setState(-firstNumber,-(value, event, [operatorValue]) => {⋮-},-[operator],-);-15const setCalculatorState = gg.setState(
Now we have a more maintainable way to handle and update the state for our calculator without having to care about dependencies between states.
Next up, let's get to the main point: Actually calculating the result!