Building a Calculator from Scratch in React

yasledesma

Yasmin Ledesma

Posted on February 11, 2022

Building a Calculator from Scratch in React

Introduction

Well, saying you're making a calculator from scratch is a hyperbole when you're working with a library like React. What I really mean here is that, while building this project, my intention was to make my own calculation algorithm, so I could later implement it as part of my app's inner workings. And I did! Here is it.

Please keep in mind that this is by no means a guide to build a calculator in React. This whole article is simply my way of documenting my experience doing it, the issues I found along the way, and the solutions I came up with to solve them.

Table of Contents

Overview of the challenge

The original challenge can be found at Frontend Mentor. To summarize it, the guys and gals at Frontend Mentor challenge you to make a calculator that performs basic operations, with a layout where you can alternate between three themes, and to make it responsive on top of that. There's also a bonus challenge if you want to allow the user to set a preferred theme color scheme.

I had originally built a calculator algorithm a few months ago. It was a series of functions that were able to perform basic calculations (addition, subtraction, division, multiplication, exponentiation, and factorials) every time you called one of them with an array of random numbers as input; the exception being the factorial function. So, something like this:

// Takes the first number and calculates it's power to the rest of the numbers inputed.
const power = (...numbers) => {
  // Takes the first number off the list and stores it in a new variable.
  const firstNumber = numbers.splice(0, 1);
  let multiplyTheRest = 1;
  let result = 1;
  // Takes the numbers array and multiplies each one times the next.
  for (let i = 0; i < numbers.length; i++) {
    multiplyTheRest *= numbers[i];
  }
  // Multiplies the first number inside firstNumber by itself as many times whatever value was outputed in the previous loop.
  for (let i = 1; i <= multiplyTheRest; i++) {
    result *= firstNumber;
  }
  return result;
};
Enter fullscreen mode Exit fullscreen mode

I found this old repo of mine gathering dust a week ago, and decided to take on the challenge to put what I learned then to use, with my own touch. Hence, why the final product doesn't look or behave exactly as the challenge's prompt. And while I had to make a few changes to make these functions work with React state and my current knowledge in it, I still kept most of them the way they originally were.

The process

Challenge N° 1: Making grid put everything where I want to... and failing miserably.

I'm not going to lie. This one was kind of a though one in the visual area. Not because it was particularly difficult to style in Sass, but because after making two functions that build and returned all my buttons, I was left with an unordered grid of 20 elements (some of them bigger than the rest.)

My first idea to make the keypad resemble the one of an actual calculator was to use the grid-template-area property on my parent element, and then give each group of related children the same grid-area name. This turned out a failure no matter how I wrote the template. Many of my buttons always ended up either overflowing or outright disappearing from the grid, and I ended up spending the biggest chunk of time just to try and make it work, then ditching it for something else, and then going back to it again.

Lucky for me, around this time Kevin Powell had published a grid YouTube short. It was unrelated to what I was trying to accomplish, but it introduced me to the grid-column and grid-row properties which, alongside the addition of a data attribute to every single one of my buttons, helped me to finally get that annoying grid exactly the way I wanted.

Basically, I set the display of my parent element to grid, set my number of columns with grid-template-columns, and then I used a combination of those two properties I learned and span to put my problematic keys in their place, selecting them by their data-key attribute.

.Buttons__button[data-key="="] {
            grid-row: 5;
            grid-column: 2/5;
        }
Enter fullscreen mode Exit fullscreen mode

Challenge N° 2: Making the calculator actually work... and actually getting it done in a few hours!

As I previously mentioned, I already had a very basic calculation algorithm lying around my repositories, so I only had to figure out how to implement it in the app with the usestate hook.

The first problem was choosing what type of input to pass into the function I was supposed to code.

I needed to use the calc state prop not only to store my input, but also to display what the user was inputting. So I decided to go with a string, because it was easier to manipulate inside the state, and also, because I'm yet to figure out a non-convoluted way to set my state to an array of numbers interleaved with string values. Who knows, maybe I'm not seeing it, but it would be a hundred times easier to work with.

The next problem I came across was getting each of those string values into an array without having multiple digit numbers breaking up and operators getting out of place.

I solved this by taking the string input and, in one hand, filtering the operators out, and in the other hand, splitting it by its operators to get the numbers out. I stored each of the two resulting arrays into their own variable, and proceeded to combine both into const operations using map. Finally, I mapped the result again to obtain the final array newArray (yes, I ran out of names by this point) with parsed numbers, filtering any undesired values at the end. I hope this snippet speaks a little better about it:

const operators = ["+", "-", "/", "*", "^", "!"];
const numbers = nums.split(/[+-\/*^!]/)
const opts = [...nums].filter( value => operators.includes(value))

const operation = numbers.map( (num, i) => {
    if(opts[i] !== undefined) {
      return [num, opts[i]]
    } else {
      return num
    }
    }).flat().filter( value => value !== "");

const newArray = operation.map( (value, i, array )=> {
    if(!(operators.includes(value))) {
        return parseFloat(value);
    } else if ( value === array[i-1] && value === "-") {
        return parseFloat(value+array[i+1])
    } else {
        return value;
    }
  }).filter( (value, i, array) => {
    if((typeof(value) === "number" && typeof(array[i-1]) === "string") || array[i-1] === undefined || typeof value === "string") {
        return value;
    }
  })
Enter fullscreen mode Exit fullscreen mode

This piece of code basically turns this: "2+2--222"
Into this: [2, "+", 2, "-", -222]

From that point on, I just had to make a loop with newArray.length > 0 as a condition to take each of its values and perform a calculation with the next one.

Now the algorithm was ready to be used.

Challenge N° 3: Making the buttons play annoying sounds. Actually pretty easy when you shallow your pride and just use a library.

This was the easiest problem of the bunch once I stopped trying to make it happen with vanilla JS and installed Howler.

If you ever want to play a sound inside your app, you can simply open the console, run npm -i howler, import the holw object constructor, and then create a function that takes the sound source as a parameter, instantiate a new howl object with two key-value pairs inside a local variable, and then apply the play() method to it.

const playSound = (src) => {
    const sound = new Howl ({
      src,
      html5: true,
    })

    sound.play()
  }
Enter fullscreen mode Exit fullscreen mode

And with that, the app was ready to deploy.

Features left to add and issues left to solve

There are still a few issues with the algorithm's computation process when the user inputs a long operation and that operation contains an exponential calc somewhere inside it. This would definitely be solved by implementing operation precedence into the algorithm, and I already thought of a way to do it, but right now, I choose to leave that for future me to handle. That, and other feature additions like a sound and theme togglers. Also, a bit of refactoring and performance optimization.

I will update you about them once I come back to this project.

And it's done!

You can find my solution to this challenge in its repository. You can also try it out live!

Screenshot

Where to find me...

You can find me on GitHub and Twitter, where I occasionally share my experience as a beginner developer.

💖 💪 🙅 🚩
yasledesma
Yasmin Ledesma

Posted on February 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related