Build a React Calculator with Hooks and Context API
Theran Brigowatz
Posted on August 19, 2020
Originally Published March 15, 2019 for FreeCodeCamp.org
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 I dove in and got to using them, I couldn’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.
Overview
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.
Our calculator design - Not award winning by any stretch of the imagination.
The Project
Getting Started
To get started we are just going to use create-react-app. You can get started by running the following:
npx create-react-app calculator
cd calculator
npm start
File Structure & CSS
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.
Main App Structure
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;
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
prop 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;
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.
Building the Context API Provider
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 following:
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;
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;
Using the Context Provider
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;
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.
Getting Started with Hooks
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;
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 a 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.
Building Button Components
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;
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.
Completed Provider Functions
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 thebuttonValue
property inNumberButton.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 toClearButton.js
.handleBackButton
allows you to delete your previously entered characters one at a time until you get back to 0. This belongs in theBackButton.js
file.handleSetCalcFunction
is where you get your math function. It sets if you are adding, subtracting, dividing, or multiplying. It gets passed into theFunctionButton.js
file and takes in thebuttonValue
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 inNegativeButton.js
.doMath
does the Math. Finally. Since this is only a simple four function calculator it is just using simple switch function depending upon thefunctionType
that we have in state. We are usingparseInt
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;
The Finished Display
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;
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.
Conclusion
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 shone a bit of light onto something that you may not have been familiar with before. React Hooks and Context are great ways to simplify your React apps and write cleaner code.
See All the Code
If you would like to see the entire code for this project it can be found over in:
GitHub Repo
Calc-U-Later Deploy
Check out more of my articles, projects, and other content at theran.dev or on twitter.
Posted on August 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 2, 2024