Building a Calculator from Scratch in React
Yasmin Ledesma
Posted on February 11, 2022
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 process
- Features left to add and issues left to solve
- And it's done!
- Where to find me...
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;
};
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;
}
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;
}
})
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()
}
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!
Where to find me...
You can find me on GitHub and Twitter, where I occasionally share my experience as a beginner developer.
Posted on February 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.