React Tutorial: Build a Calculator App from Scratch

0

In this tutorial, we’ll create a React Calculator app. You will learn how to create a wireframe, design a layout, create components, update states, and format the output.

To inspire you, here is a link to the deployed project we will build.

Also here is the source code, just for reference if you need help at any stage of the project.

Planning

Since we are going to be creating a Calculator app, let’s choose a scope that is not too complicated for learning, but not too basic to cover the different aspects of building an app.

The features we will be implementing include:

  • add, subtract, multiply, divide
  • support decimal values
  • calculate percentages
  • reverse values
  • reset functionality
  • format larger numbers
  • resize output based on length

To get started, we’ll draw a basic wireframe to display our ideas. For this you can use free tools like Figma Where Diagrammes.net.

Note that in this phase it is not so important to think about colors and style. What matters most is that you can structure the layout and identify the components involved.

Design colors

Once we have dealt with the layout and components, all that’s left to do to finish the design is pick a nice color scheme.

Below are some guidelines to make the app attractive:

  • the packaging must contrast with the background
  • screen and button values ​​should be easy to read
  • the equal button should be a different color, to give some accent

Based on the above criteria, we will use the color scheme below.

Color scheme

Project configuration

To get started, open the terminal in your projects folder and create a boilerplate template using the create-react-application. To do this, run the command:

npx create-react-app calculator

This is the fastest and easiest way to set up a fully functional React app without any configuration. All you need to do after that is done cd calculator to switch to the newly created project folder and npm start to start your application in the browser.

Browser view

As you can see, it comes with a master key by default, so next we’re going to do some cleaning up in the project folder tree.

Find it src folder, where your application logic will live, and delete everything except App.js to create your application, index.css to style your application, and index.js to make your application in the DOM.

Project tree

Create components

Since we’ve done wireframing before, we already know the main building blocks of the app. These are Wrapper, Screen, ButtonBox, and Button.

First create a components folder inside src case. We will then create a .js file and .css file for each component.

If you don’t want to create these folders and files manually, you can use the following one-liner to quickly set things up:

cd src && mkdir components && cd components && touch Wrapper.js Wrapper.css Screen.js Screen.css ButtonBox.js ButtonBox.css Button.js Button.css

Packaging

The Wrapper component will be the frame, holding all child components in place. This will also allow us to center the entire application afterwards.

Wrapper.js

import "./Wrapper.css";

const Wrapper = ({ children }) => {
  return <div className="wrapper">{children}</div>;
};

export default Wrapper;

Wrapper.css

.wrapper {
  width: 340px;
  height: 540px;
  padding: 10px;
  border-radius: 10px;
  background-color: #485461;
  background-image: linear-gradient(315deg, #485461 0%, #28313b 74%);
}

Filter

The Screen will be the child of the upper section of the Wrapper component, and its purpose will be to display the calculated values.

In the list of features, we have included resizing the display output based on length, which means longer values ​​should decrease in size. We will be using a small library (3.4kb gzip) called react-textfit for that.

To install it, run npm i react-textfit then import it and use it as shown below.

Screen.js

import { Textfit } from "react-textfit";
import "./Screen.css";

const Screen = ({ value }) => {
  return (
    <Textfit className="screen" mode="single" max={70}>
      {value}
    </Textfit>
  );
};

export default Screen;

Screen.css

.screen {
  height: 100px;
  width: 100%;
  margin-bottom: 10px;
  padding: 0 10px;
  background-color: #4357692d;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: white;
  font-weight: bold;
  box-sizing: border-box;
}

ButtonBox

The ButtonBox component, in the same way as the Wrapper component, will be the framework for the children – only this time for the Button Components.

ButtonBox.js

import "./ButtonBox.css";

const ButtonBox = ({ children }) => {
  return <div className="buttonBox">{children}</div>;
};

export default ButtonBox;

ButtonBox.css

.buttonBox {
  width: 100%;
  height: calc(100% - 110px);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(5, 1fr);
  grid-gap: 10px;
}

Button

The Button component will provide interactivity for the application. Each component will have the value and onClick accessories.

In the stylesheet, we will also include the styles for the equal button. we will use Button props to enter the classroom later.

Button.js

import "./Button.css";

const Button = ({ className, value, onClick }) => {
  return (
    <button className={className} onClick={onClick}>
      {value}
    </button>
  );
};

export default Button;

Button.css

button {
  border: none;
  background-color: rgb(80, 60, 209);
  font-size: 24px;
  color: rgb(255, 255, 255);
  font-weight: bold;
  cursor: pointer;
  border-radius: 10px;
  outline: none;
}

button:hover {
  background-color: rgb(61, 43, 184);
}

.equals {
  grid-column: 3 / 5;
  background-color: rgb(243, 61, 29);
}

.equals:hover {
  background-color: rgb(228, 39, 15);
}

Render elements

The base file for rendering in React apps is index.js. Before going any further, make sure that your index.js looks like this:

import React from "react";
import ReactDOM from "react-dom";

import App from "./App";
import "./index.css";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

Also check index.css and make sure to reset the default values ​​for padding and margin, choose a good font (like Montserrat in this case) and define the appropriate rules to center the application in the window:

@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");

* {
  margin: 0;
  padding: 0;
  font-family: "Montserrat", sans-serif;
}

body {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #fbb034;
  background-image: linear-gradient(315deg, #fbb034 0%, #ffdd00 74%);
}

Finally, let’s open the main file App.js, and import all the components we created previously:

import Wrapper from "./components/Wrapper";
import Screen from "./components/Screen";
import ButtonBox from "./components/ButtonBox";
import Button from "./components/Button";

const App = () => {
  return (
    <Wrapper>
      <Screen value="0" />
      <ButtonBox>
        <Button
          className=""
          value="0"
          onClick={() => {
            console.log("Button clicked!");
          }}
        />
      </ButtonBox>
    </Wrapper>
  );
};

export default App;

In the example above, we have rendered only one Button making up.

Let’s create a matrix representation of the data in the wireframe, so that we can map and display all the buttons in the ButtonBox:

import Wrapper from "./components/Wrapper";
import Screen from "./components/Screen";
import ButtonBox from "./components/ButtonBox";
import Button from "./components/Button";

const btnValues = [
  ["C", "+-", "%", "https://www.sitepoint.com/"],
  [7, 8, 9, "X"],
  [4, 5, 6, "-"],
  [1, 2, 3, "+"],
  [0, ".", "="],
];

const App = () => {
  return (
    <Wrapper>
      <Screen value=0 />
      <ButtonBox>
        {
          btnValues.flat().map((btn, i) => {
            return (
              <Button
                key={i}
                className={btn === "=" ? "equals" : ""}
                value={btn}
                onClick={() => {
                  console.log(`${btn} clicked!`);
                }}
              />
            );
          })
        }
      </ButtonBox>
    </Wrapper>
  );
};

Check your terminal and make sure your React app is still running. Otherwise, run npm start to start it again.

Open your browser. If you followed, your current result should look like this:

Application design

If you want, you can also open the browser developer tools and test the log values ​​for each button you press.

Console.log

Define states

Next, we’ll declare the state variables using React useState to hang up.

More precisely, there will be three states: num, the value entered; sign, the selected sign: and res, the calculated value.

In order to use the useState hook, we first need to import it into App.js:

import React, { useState } from "react";

In the App function, we will use an object to define all the states at once:

import React, { useState } from "react";



const App = () => {
  let [calc, setCalc] = useState({
    sign: "",
    num: 0,
    res: 0,
  });

  return (
    
  );
};

Functionality

Our app looks nice, but there is no functionality. Currently, it can only output button values ​​in the browser console. Let’s fix this!

We will start with the Screen making up. Set the following conditional logic to value prop, so it displays the entered number (if the number is typed) or the calculated result (if the equal button is pressed).

For this we will use the integrated JS ternary operator, which is essentially a shortcut for the if statement, taking an expression and returning a value after ? if the expression is true, or after : if the expression is false:

<Screen value={calc.num ? calc.num : calc.res} />

Now let’s change the Button component so that it can detect different types of buttons and perform the assigned function once the specific button is pressed. Use the code below:

import React, { useState } from "react";



const App = () => {
  

  return (
    <Wrapper>
      <Screen value={calc.num ? calc.num : calc.res} />
      <ButtonBox>
        {btnValues.flat().map((btn, i) => {
          return (
            <Button
              key={i}
              className={btn === "=" ? "equals" : ""}
              value={btn}
              onClick={
                btn === "C"
                  ? resetClickHandler
                  : btn === "+-"
                  ? invertClickHandler
                  : btn === "%"
                  ? percentClickHandler
                  : btn === "="
                  ? equalsClickHandler
                  : btn === "https://www.sitepoint.com/" || btn === "X" || btn === "-" || btn === "+"
                  ? signClickHandler
                  : btn === "."
                  ? commaClickHandler
                  : numClickHandler
              }
            />
          );
        })}
      </ButtonBox>
    </Wrapper>
  );
};

We are now ready to create all the necessary functions.

numClickHandler

The numClickHandler The function is only triggered if one of the number keys (0 to 9) is pressed. He then obtains the value of Button and add that to the current num value.

He will also ensure that:

  • no integer starts with zero
  • there are not several zeros before the decimal point
  • the format will be “0”. if “.” is pressed first
  • numbers are entered up to 16 integers
import React, { useState } from "react";



const App = () => {
  

  const numClickHandler = (e) => {
    e.preventDefault();
    const value = e.target.innerHTML;

    if (calc.num.length < 16) {
      setCalc({
        ...calc,
        num:
          calc.num === 0 && value === "0"
            ? "0"
            : calc.num % 1 === 0
            ? Number(calc.num + value)
            : calc.num + value,
        res: !calc.sign ? 0 : calc.res,
      });
    }
  };

  return (
    
  );
};

commaClickHandler

The commaClickHandler the function is only triggered if the decimal point (.) is pressed. It adds the decimal point to the current num value, making it a decimal number.

It will also make sure that no multiple decimal point is possible.

Note: I called the handler function “commaClickHandler” because in many parts of the world integers and decimals are separated by a comma, not a decimal point.



const commaClickHandler = (e) => {
  e.preventDefault();
  const value = e.target.innerHTML;

  setCalc({
    ...calc,
    num: !calc.num.toString().includes(".") ? calc.num + value : calc.num,
  });
};

signClickHandler

The signClickHandler the function is triggered when the user presses either +, , * Where /. The particular value is then defined as a current sign value in the calc object.

This will also ensure that there is no effect on repeated calls:



const signClickHandler = (e) => {
  e.preventDefault();
  const value = e.target.innerHTML;

  setCalc({
    ...calc,
    sign: value,
    res: !calc.res && calc.num ? calc.num : calc.res,
    num: 0,
  });
};

equal to ClickHandler

The equalsClickHandler The function calculates the result when the button equals (=) is pressed. The calculation is based on the current num and res value, as well as the sign selected (see math function).

The returned value is then defined as the new res for subsequent calculations.

He will also ensure that:

  • there is no effect on repeated calls
  • users cannot divide with 0


const equalsClickHandler = () => {
  if (calc.sign && calc.num) {
    const math = (a, b, sign) =>
      sign === "+"
        ? a + b
        : sign === "-"
        ? a - b
        : sign === "X"
        ? a * b
        : a / b;

    setCalc({
      ...calc,
      res:
        calc.num === "0" && calc.sign === "https://www.sitepoint.com/"
          ? "Can't divide with 0"
          : math(Number(calc.res), Number(calc.num), calc.sign),
      sign: "",
      num: 0,
    });
  }
};

invertClickHandler

The invertClickHandler The function first checks if there is an entered value (num) or calculated value (res) then reverse them by multiplying them by -1:



const invertClickHandler = () => {
  setCalc({
    ...calc,
    num: calc.num ? calc.num * -1 : 0,
    res: calc.res ? calc.res * -1 : 0,
    sign: "",
  });
};

percentClickHandler

The percentClickHandler the function checks if there is an entered value (num) or calculated value (res) then calculate the percentage using the built-in function Math.pow function, which returns the base to the power of the exponent:



const percentClickHandler = () => {
  let num = calc.num ? parseFloat(calc.num) : 0;
  let res = calc.res ? parseFloat(calc.res) : 0;

  setCalc({
    ...calc,
    num: (num /= Math.pow(100, 1)),
    res: (res /= Math.pow(100, 1)),
    sign: "",
  });
};

resetClickHandler

The resetClickHandler the function defaults to all the initial values ​​of calc, by turning over the calc Status as it was when the Calculator app was first rendered:



const resetClickHandler = () => {
  setCalc({
    ...calc,
    sign: "",
    num: 0,
    res: 0,
  });
};

Input formatting

One more thing to complete the intro’s feature list would be to implement value formatting. For this we could use a modified Regex string published by Emissary:

const toLocaleString = (num) =>
  String(num).replace(/(?/g, "$1 ");

Basically what it does is take a number, format it as a string, and create the space separators for the thousands mark.

If we reverse the process and want to process the string of numbers, we need to remove the spaces first so that we can then convert it to a number. For that, you can use this function:

const removeSpaces = (num) => num.toString().replace(/s/g, "");

Here is the code where you need to include the two functions:

import React, { useState } from "react";



const toLocaleString = (num) =>
  String(num).replace(/(?/g, "$1 ");

const removeSpaces = (num) => num.toString().replace(/s/g, "");

const App = () => {
  

  return (
    
  );
};

Check out the next section with the full code on how to add toLocaleString and removeSpaces as manager for the Button making up.

Put it all together

If you followed, the whole App.js the code should look like this:

import React, { useState } from "react";

import Wrapper from "./components/Wrapper";
import Screen from "./components/Screen";
import ButtonBox from "./components/ButtonBox";
import Button from "./components/Button";

const btnValues = [
  ["C", "+-", "%", "https://www.sitepoint.com/"],
  [7, 8, 9, "X"],
  [4, 5, 6, "-"],
  [1, 2, 3, "+"],
  [0, ".", "="],
];

const toLocaleString = (num) =>
  String(num).replace(/(?/g, "$1 ");

const removeSpaces = (num) => num.toString().replace(/s/g, "");

const App = () => {
  let [calc, setCalc] = useState({
    sign: "",
    num: 0,
    res: 0,
  });

  const numClickHandler = (e) => {
    e.preventDefault();
    const value = e.target.innerHTML;

    if (removeSpaces(calc.num).length < 16) {
      setCalc({
        ...calc,
        num:
          calc.num === 0 && value === "0"
            ? "0"
            : removeSpaces(calc.num) % 1 === 0
            ? toLocaleString(Number(removeSpaces(calc.num + value)))
            : toLocaleString(calc.num + value),
        res: !calc.sign ? 0 : calc.res,
      });
    }
  };

  const commaClickHandler = (e) => {
    e.preventDefault();
    const value = e.target.innerHTML;

    setCalc({
      ...calc,
      num: !calc.num.toString().includes(".") ? calc.num + value : calc.num,
    });
  };

  const signClickHandler = (e) => {
    e.preventDefault();
    const value = e.target.innerHTML;

    setCalc({
      ...calc,
      sign: value,
      res: !calc.res && calc.num ? calc.num : calc.res,
      num: 0,
    });
  };

  const equalsClickHandler = () => {
    if (calc.sign && calc.num) {
      const math = (a, b, sign) =>
        sign === "+"
          ? a + b
          : sign === "-"
          ? a - b
          : sign === "X"
          ? a * b
          : a / b;

      setCalc({
        ...calc,
        res:
          calc.num === "0" && calc.sign === "https://www.sitepoint.com/"
            ? "Can't divide with 0"
            : toLocaleString(
                math(
                  Number(removeSpaces(calc.res)),
                  Number(removeSpaces(calc.num)),
                  calc.sign
                )
              ),
        sign: "",
        num: 0,
      });
    }
  };

  const invertClickHandler = () => {
    setCalc({
      ...calc,
      num: calc.num ? toLocaleString(removeSpaces(calc.num) * -1) : 0,
      res: calc.res ? toLocaleString(removeSpaces(calc.res) * -1) : 0,
      sign: "",
    });
  };

  const percentClickHandler = () => {
    let num = calc.num ? parseFloat(removeSpaces(calc.num)) : 0;
    let res = calc.res ? parseFloat(removeSpaces(calc.res)) : 0;

    setCalc({
      ...calc,
      num: (num /= Math.pow(100, 1)),
      res: (res /= Math.pow(100, 1)),
      sign: "",
    });
  };

  const resetClickHandler = () => {
    setCalc({
      ...calc,
      sign: "",
      num: 0,
      res: 0,
    });
  };

  return (
    <Wrapper>
      <Screen value={calc.num ? calc.num : calc.res} />
      <ButtonBox>
        {btnValues.flat().map((btn, i) => {
          return (
            <Button
              key={i}
              className={btn === "=" ? "equals" : ""}
              value={btn}
              onClick={
                btn === "C"
                  ? resetClickHandler
                  : btn === "+-"
                  ? invertClickHandler
                  : btn === "%"
                  ? percentClickHandler
                  : btn === "="
                  ? equalsClickHandler
                  : btn === "https://www.sitepoint.com/" || btn === "X" || btn === "-" || btn === "+"
                  ? signClickHandler
                  : btn === "."
                  ? commaClickHandler
                  : numClickHandler
              }
            />
          );
        })}
      </ButtonBox>
    </Wrapper>
  );
};

export default App;

Final Notes

Congratulations! You have created a fully functional and stylish application. Hope you learned a thing or two in the process!

Demo

Other ideas to explore would be adding scientific functionality or implementing memory with the list of previous calculations.

If you have any problem reports or feature requests, please leave them in the GitHub repository. If you like the project, feel free to start it.


Source link

Share.

Leave A Reply