Building a Calculator with React Hooks

Building a Calculator with React Hooks

  • 2019-03-15 03:39 AM
  • 265

If you are like me, when you first heard of React Hooks you were maybe a little bit ambivalent or confused by what all the hype was about

What’s the big deal if I don’t have to write out class components anymore? However once you dive in and get to using them, I can’t really see myself going back to my pre-Hook days. In the immortal words of Blues Traveller, “The hook brings you back. I ain’t tellin’ you no lie.”

For a while, I was looking for a guide about how to use Hooks in connection with the Context API. After only finding a few examples out there that explained the concept fully, I decided to do what any good developer should do; pore over the docs and build something myself. Struggling through it and learning it on your own is one of the best ways to absorb knowledge. This is a guide for how to build the same project that I did with the use of Hooks and Context.

This project is going to be a basic calculator app similar to the iPhone calculator. Since this is just a simple desktop app I have replaced the % button with a back button. Though I wouldn’t use this to take the SATs, you could definitely add up the number of toes you have on it.

There is a working deployed version of the project, or you can view all of the code on GitHub.

To get started we are just going to use create-react-app. You can get started by running the following:

npx create-react-app calculatorcd calculatornpm start

The file structure of the app should look like the following. In the src folder create the following files or just leave the App.js and index.js.

src
├── App.js
├── index.js
└── components
    ├── BackButton.js
    ├── Calculator.js
    ├── ClearButton.js
    ├── Display.js    
    ├── EqualButton.js
    ├── FunctionButton.js
    ├── NegativeButton.js
    ├── NumberButton.js
    ├── NumberProvider.js
    └── styles
        └── Styles.js

If you want to follow along exactly you can also install Styled Components for the CSS.

npm -i styled-components

You can then add the Styled CSS from this link to the Styles.js file or add your own.

The Calculator.js file should setup the display and number pad. It should contain all of the button types.

import React from 'react';
import NumberButton from './NumberButton';
import FunctionButton from './FunctionButton';
import ClearButton from './ClearButton';
import Display from './Display';
import EqualButton from './EqualButton';
import BackButton from './BackButton';
import NegativeButton from './NegativeButton';
import { CalculatorStyles } from './styles/Styles';

const Calculator = () => (
  <CalculatorStyles>
    <div className="display">
      <h1>CALC-U-LATER</h1>
      <Display />
    </div>
    <div className="number-pad">
      <ClearButton />
      <BackButton />
      <NegativeButton />
      <FunctionButton buttonValue="/" />
      <NumberButton buttonValue={7} />
      <NumberButton buttonValue={8} />
      <NumberButton buttonValue={9} />
      <FunctionButton buttonValue="*" />
      <NumberButton buttonValue={4} />
      <NumberButton buttonValue={5} />
      <NumberButton buttonValue={6} />
      <FunctionButton buttonValue="-" />
      <NumberButton buttonValue={1} />
      <NumberButton buttonValue={2} />
      <NumberButton buttonValue={3} />
      <FunctionButton buttonValue="+" />
      <div className="zero-button">
        <NumberButton buttonValue={0} />
      </div>
      <NumberButton buttonValue="." />
      <EqualButton />
    </div>
  </CalculatorStyles>
);

export default Calculator;

Caclulator.js

You will notice that all of the button components are added in here along with the number display. Each of the button components are essentially the same. They should all follow the same basic structure. The zero-button gets a separate div since we are using CSS Grid for the layout and it needs to span two columns. (PS — If you want to know more about CSS Grid I did a little article on the basics.)

You may notice that the buttonValue props is only needed for the NumberButton and FunctionButton components. Each of the buttons should follow the same basic structure with a unique name. You can reference the file structure up above to see which buttons are needed. The buttons should have the symbol written in the button component if they are not passed a buttonValue via props. Create one of these for each of the button types in your file structure.

import React from 'react';

const ButtonName = ({ buttonValue }) => {
  return (
    <button type="button">{buttonValue}</button>
  );
};

export default ButtonName;

BasicButton.js

After this you should have the basic structure of a calculator. We are going to come back to the display in just a bit. Now we are going to get into the inner workings of the app and see how we can use our Hooks and Context.

We are now going to create the NumberProvider.js . This is the heart of your app and where our functions are going to live. If you have never used the React Context API it is a great tool to help pass data from one component to another.

Think of when you have components that are nested within each other. In the past you would have to “prop drill” . This is when you pass the data or function through as props in down through nested components. This is hardly ideal, especially when you start to go several layers deep.

However, with this provider component, it allows you to pass data to any nested component, no matter how deep. This number provider will wrap our App component. Now whenever we want to get data, or use a function that lives in the provider, it is globally available. This gets us out of having to “prop drill” through nested components. You maintain the single source of truth that is the essence of React. To get started you need to create the provider. It should look like the folowing:

import React from 'react';

export const NumberContext = React.createContext();

const NumberProvider = props => {
  const number = "0"
  return (
    <NumberContext.Provider
      value={{
        number
      }}
    >
      {props.children}
    </NumberContext.Provider>
  );
};

export default NumberProvider;

BasicProvider.js

The basic provider is created and any value that is passed in is now available to all nested components. In order to make this available we are going to wrap our App component so it is globally available. Our App will have this code.

import React from 'react';
import Calculator from './components/Calculator';
import NumberProvider from './components/NumberProvider';

const App = () => (
  <NumberProvider>
    <Calculator />
  </NumberProvider>
);

export default App;

App.js
Now we can add in the code for our display. We can display the value by passing in the useContext function from the new React Hooks API. We no longer have to pass in prop through nested components. The display should look like:

import React, { useContext } from 'react';
import { NumberContext } from './NumberProvider';
import { DisplayStyles } from './styles/Styles';

const Display = () => {
  const { number } = useContext(NumberContext);
  return (
    <DisplayStyles>
      <h2>{number}</h2>
      <p>Enter Some Numbers</p>
    </DisplayStyles>
  );
};

export default Display;

Display.js

The number that you passed three levels up in the NumberProvider is immediately available to the Display component by calling useContext and passing our created NumberContext. Your number display is now up and running as it is showing number which we have set to zero.

Now of course our calculator is showing a single zero. This is great if you are counting the number of hours of sleep I get with a new born son, but not so great if trying to add anything else, so let’s use some hooks going get this calculator calculating.

If you haven’t used a hook before, it essentially allows you to get rid of the class syntax, and instead have state within functional components. Here we can add the following to our NumberProvider.js file in order to create our first hook.

import React, { useState } from 'react';

export const NumberContext = React.createContext();

const NumberProvider = props => {
  const [number, setNumber] = useState('');

  const handleSetDisplayValue = num => {
    if (!number.includes('.') || num !== '.') {
      setNumber(`${(number + num).replace(/^0+/, '')}`);
    }
  };

  return (
    <NumberContext.Provider
      value={{
        handleSetDisplayValue,
        number
      }}
    >
      {props.children}
    </NumberContext.Provider>
  );
};

export default NumberProvider;

NumberProvider.js

There might be some syntax you have not seen. Rather than writing out our class with state we break each part of state into its own smaller number variable. There is also setNumber which acts the same as setState function, but now works for a specific variable, and can be called when necessary. useState allows us to set an initial value.

We are now able to use this all in our function to pass the number button values into the display. In this app the calculator is using strings to get the input. There are checks to make sure that you can not have multiple . in your number and that you do not have series of zeroes to start your number.

Now you can call this function using the Context API in any of the nested components.

import React, { useContext } from 'react';
import { NumberContext } from './NumberProvider';

const NumberButton = ({ buttonValue }) => {
  const { handleSetDisplayValue } = useContext(NumberContext);
  return (
    <button type="button" onClick={() => handleSetDisplayValue(buttonValue)}>
      {buttonValue}
    </button>
  );
};

export default NumberButton;

NumberButton.js

Now you have working string of numbers maker. You can see how you can start to inject the values that you set in the NumberProvider into the other components of the app via the useContext function. State and the functions that affect it are held in the NumberProvider . You just have to call in the specific context that you want. You can start to see how this would be great as you start to add more complexity to your app. Say you want a user component to check that you are logged in to use special features. You can create a separate provider that holds the user data and makes that available any nested component.

We can continue to add in functions to our calculator and pass them to the proper component through the useContext function that is built in.

The completed NumberProvider is found below and contains the following functions that are used with hooks.

  • handleSetDisplayValue sets the value that you are typing into the display. We are checking that it there is only one decimal in the number string and we are limiting the number length to 8 characters. Think of this as more a tip calculator than one to get you through your calculus exam. It takes in the buttonValue property in NumberButton.js .
  • handleSetStoredValue takes our display string and stores it so that we can enter another number. This is our stored value. It will be used as a helper function.
  • handleClearValue resets everything back to 0. This is your clear function. It will get passed to ClearButton.js.
  • handleBackButton allows you to delete your previously entered characters one at a time until you get back to 0. This belongs in the BackButton.js file.
  • handleSetCalcFunction is where you get your math function. It sets if you are adding, subtracting, dividing, or multiplying. It gets passed into the FunctionButton.js file and takes in the buttonValue property.
  • handleToggleNegative does just as the name implies. It allows you do so for either the display value or a stored value after a calculation. This of course goes in NegativeButton.js.
  • doMath does the Math. Finally. Since this is only a simple four function calculator it is just using simple switch function depending upon the functionType that we have in state. We are using parseInt since we are passing our number in as strings. Also we are rounding to only three decimal places, to make sure that we do not have crazy long numbers.
import React, { useState } from 'react';

export const NumberContext = React.createContext();

const NumberProvider = props => {
  const [number, setNumber] = useState('');
  const [storedNumber, setStoredNumber] = useState('');
  const [functionType, setFunctionType] = useState('');

  const handleSetDisplayValue = num => {
    if ((!number.includes('.') || num !== '.') && number.length < 8) {
      setNumber(`${(number + num).replace(/^0+/, '')}`);
    }
  };

  const handleSetStoredValue = () => {
    setStoredNumber(number);
    setNumber('');
  };

  const handleClearValue = () => {
    setNumber('');
    setStoredNumber('');
    setFunctionType('');
  };

  const handleBackButton = () => {
    if (number !== '') {
      const deletedNumber = number.slice(0, number.length - 1);
      setNumber(deletedNumber);
    }
  };

  const handleSetCalcFunction = type => {
    if (number) {
      setFunctionType(type);
      handleSetStoredValue();
    }
    if (storedNumber) {
      setFunctionType(type);
    }
  };

  const handleToggleNegative = () => {
    if (number) {
      if (number > 0) {
        setNumber(`-${number}`);
      } else {
        const positiveNumber = number.slice(1);
        setNumber(positiveNumber);
      }
    } else if (storedNumber > 0) {
      setStoredNumber(`-${storedNumber}`);
    } else {
      const positiveNumber = storedNumber.slice(1);
      setStoredNumber(positiveNumber);
    }
  };

  const doMath = () => {
    if (number && storedNumber) {
      switch (functionType) {
        case '+':
          setStoredNumber(`${Math.round(`${(parseFloat(storedNumber) + parseFloat(number)) * 100}`) / 100}`);
          break;
        case '-':
          setStoredNumber(`${Math.round(`${(parseFloat(storedNumber) - parseFloat(number)) * 1000}`) / 1000}`);
          break;
        case '/':
          setStoredNumber(`${Math.round(`${(parseFloat(storedNumber) / parseFloat(number)) * 1000}`) / 1000}`);
          break;
        case '*':
          setStoredNumber(`${Math.round(`${parseFloat(storedNumber) * parseFloat(number) * 1000}`) / 1000}`);
          break;
        default:
          break;
      }
      setNumber('');
    }
  };

  return (
    <NumberContext.Provider
      value={{
        doMath,
        functionType,
        handleBackButton,
        handleClearValue,
        handleSetCalcFunction,
        handleSetDisplayValue,
        handleSetStoredValue,
        handleToggleNegative,
        number,
        storedNumber,
        setNumber,
      }}
    >
      {props.children}
    </NumberContext.Provider>
  );
};

export default NumberProvider;

NumberProvider.js

You will also need a display. In this case it will show the number and the storedNumber along with your functionType. There are a few check such as showing a 0 when you have an empty string as a number.

import React, { useContext } from 'react';
import { NumberContext } from './NumberProvider';
import { DisplayStyles } from './styles/Styles';

const Display = () => {
  const { number, storedNumber, functionType } = useContext(NumberContext);
  return (
    <DisplayStyles>
      <h2>{!number.length && !storedNumber ? '0' : number || storedNumber}</h2>
      <p>{!storedNumber ? 'ENTER SOME NUMBERS' : `${storedNumber} ${functionType} ${number}`}</p>
    </DisplayStyles>
  );
};

export default Display;

Display.js

For brevity sake I am not going to include all of the button functions since they are pretty much the same as the NumberButton.js file above. Just be sure that you pass in a buttonValue prop when necessary, and that you are passing in the correct function from the above list.

If you would like to see the entire code it can be found over in this

GitHub Repo

Calc-U-Later Deploy

I hope that this clears up a bit about how React Hooks and the Context API can be used together. Using these built in React features offers several benefits.

  • Simple to understand syntax and gets rid of the clutter of class components. No more super and constructors. Just a few clean variables.
  • Easier to set and use state within and across components. No more messy prop drilling through multiple component.
  • Eliminates need for Redux in small projects, where you don’t need to hold too much in complex state. You probably aren’t going to re-create Facebook with it, but it will do the job on small-scale apps.

Please let me know your thoughts or if there are any issues that you come across in the code. Hopefully this shined a bit of life onto something that you may not have been familiar with before.

Check out more of my work at https://theran.co.

Recommended Courses:

The Complete Web Development Tutorial Using React and Redux

React Hooks

React From The Ground Up

React for visual learners

Hello React - React Training for JavaScript Beginners