This is what we are going to require in terms of React and React Native
React Native + .63
React 16+
If you haven’t worked with RN before, I suggest you follow the guide below to get you up to speed. Remember to do it with react-native cli since we are not using expo this time. Also make sure you select the right OS.
https://reactnative.dev/docs/environment-setup
Here’s what you’ll need:
- node 8+
- [Homebrew](https://brew.sh/)
- [Watchman](https://facebook.github.io/watchman)
- Xcode
- Android Studio
- CocoaPods
If you need any further help or a closer look into this, the repo for this implementation is available here: https://github.com/c3sarr0driguez/rn_basic_calculator
Let’s get started.
npx react-native init {ProjectName} && cd {ProjectName}
Example:
npx react-native init BasicCalculator && cd BasicCalculator
After this, we are going to follow best practices and start building. First, we need a folder structure like this:
This is what we are going to build today.

## Folder Structure
src | |_ assets |_ components |_text... |_calcButton... |_ pages | |_main |_ utils | |_constants | |_styles |_contexts
<strong> Since this is a technical tutorial, I won’t spend time explaining styling. But, I will include them in this exercise. </strong>
We are going to use yarn to install dependencies
First we are going to integrate everything speaking about developer environment
To have a good linter we need to install
yarn add --dev eslint prettier @react-native-community/eslint-config
And then add to your eslint config (`.eslintrc`, or `eslintConfig` field in `package.json`):
{ "extends": "@react-native-community", "settings": { "import/resolver": { "babel-module": {} } }, }
this will install prettier and eslint configurations so you can be now able to detect issues and code styling issues in your code , for this step we are going to include then
{ "scripts": { ... "lint": "eslint ./src" } }
After that we are going to install babel module resolver to make absolute imports in our project
yarn add –dev eslint-plugin-import eslint-import-resolver-babel-module
babel-plugin-module-resolver
and compliment your `.babelrc`or `babel.config.js` with
{ "plugins": [ ["module-resolver", { "root": ["./src"], }] ] }
Finally for the development you will add the proper config for our editor to visualize the imports correctly by adding a file ‘jsconfig.json’ With the following content
{ "compilerOptions": { "baseUrl": ".", "paths": { "*": ["src/*"] } } }
Before we start, we are going to install the libs we missed
responsive 'yarn add react-native-responsive-screen'
# Hands on
We are going to first add a screen to pages under the name of main.js and the style file main.styles.js with empty content
pages |__main |__main.js |__main.styles.js ```
### App.js
import React from 'react'; import Main from 'screens/main/main'; function App() { return ( <Main/> ); } export default App;
### Components
We are going to start developing our shared(universal components) which are text calcButton and mainButton
### Important!
All components we are going to build will have more or less the very same structure. If our styles are complex to add, we need to abstract that separation of concern into a separate function. For example, let’s say that you have a button component that you need to have different behaviors through properties:
const SIZE = { SM: 'sm', MD: 'md', } const STYLE = { BOLD: 'bold', ITALIC: 'italic', UPPERCASE: 'uppercase', } const VARIANT = { PRIMARY: 'primary', SECONDARY: 'secondary', SOMETHING: 'something', }
This is how we should abstract all the logic. Otherwise, the button will be hard to read. So, this hypothetic function should deliver all the styles compounded. If you have a complex component with subcomponents that can be themselves customized, then you should not deliver just one compounded object but several like
componentStyles = { text: textStyles, button: buttonStyles }
return componentStyles
Then assign this to the right components
Let’s take the following function
function mapStateAndPropsToStyles({color, textColor, style}) { const componentStyles = [styles.calcButton]; if (color) { componentStyles.push({backgroundColor: color}); } if (textColor) { componentStyles.push({color: textColor}); } if (style) { if (Array.isArray(style)) { componentStyles.push(...style); } else { componentStyles.push(style); } } return componentStyles; }
So, as we can see, we start assigning the baseStyles.
`const componentStyles = [styles.calcButton];`
If you are going to have more stuff like the variants we mentioned earlier, you can make use of optionalParameters. With this, you can set a default value and have assurance that your style will have all the properties you want (may not be explicit)
Let’s say
function mapStateAndPropsToStyle({variant=VARIANT.PRIMARY, size=SIZE.MD}) { ... }
Or, you can use defaultProps to do the same thing.
So, remember that all of our components will use the same technique.
Finally, all of our components may or may not have a drill props function in case we are aliasing or controlling how children components behave. Let’s say that under our button we have a text that has its own properties like size and color variants because we have 3 different combination of variables that we do not want our users to experiment with, at least not freely due to style concerns or requirements. Then we are going to have another abstraction of those to decorator that ask for a property.
Let’s say
const TEXT_VARIANT = { PRIMARY: 'FIRST', SECONDARY: 'SECONDARY' }
Let’s suppose now that the first variant will have
* size sm
* variant bold
* color white
You see the problem?
This is why we are going to map our state/props to text props, to control the text children
function drillProps({variant}) { if(variant === VARIANTS.PRIMARY) { return { color: 'red', size: 'md' }; } else if() { return { color: 'blue', size: 'sm' }; } else { throw TypeError(`Not implemented variant ${variant}` } }
So, let’s recap.
Since the structure of all of these components is the very same, we are not going to explain any more about these capabilities.
We have a function to translate state and props to complex styles that could return one object for the root component, and if needed more to be mapped to several components
‘function mapStateAndPropsToStyles’
The function to decorate other component’s through an interface that will take control over the children elements but will expose certain functionalities to change the content like inner text
‘function drillDownStateAndProps’
Do not get stuck with the names, just be consistent.
###CalcButton.js
function CalcButton({ color, textColor, onPress, children, textProps = {}, style, }) { return ( <TouchableOpacity onPress={onPress} style={mapStateAndPropsToStyles({ color, textColor, style, })}> <Text {...drillsDownStateAndprops({textColor})} {...textProps}> {children} </Text> </TouchableOpacity> ); }
Simple right?
We are making use of ‘drillsDownStateAndprops’ and ‘mapStateAndPropsToStyles’ as we discussed. So, this is the complete code.
// libs
import React, {useContext, memo} from 'react'; import PropTypes from 'prop-types'; import {TouchableOpacity} from 'react-native'; import Text from 'components/text'; import ThemeContext from 'contexts/themes'; // styles import styles from './calcButton.styles'; function mapStateAndPropsToStyles({color, textColor, style}) { const componentStyles = [styles.calcButton]; if (color) { componentStyles.push({backgroundColor: color}); } if (textColor) { componentStyles.push({color: textColor}); } if (style) { if (Array.isArray(style)) { componentStyles.push(...style); } else { componentStyles.push(style); } } return componentStyles; } function drillDownStateAndProps({textColor}) { return { color: textColor, }; } function CalcButton(color, textColor, onPress, children, textProps = {}, style) { return ( <TouchableOpacity onPress={onPress} style={mapStateAndPropsToStyles({ color, textColor, style, })}> <Text {...drillDownStateAndProps({textColor})} {...textProps}> {children} </Text> </TouchableOpacity> ); } // We make use of proptypes to provide a strong component signature CalcButton.propTypes = { onPress: PropTypes.func, textColor: PropTypes.string, textProps: PropTypes.object, color: PropTypes.string, style: PropTypes.oneOfType([ PropTypes.number, PropTypes.object, PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.object), ]), }; export default memo(CalcButton); ### CalcButton.styles.js import {StyleSheet} from 'react-native'; import {COLOR} from 'utils/styles'; const baseStyles = { calcButton: { flex: 1, justifyContent: 'center', alignItems: 'center', color: COLOR.BLACK, }, }; const styles = StyleSheet.create(baseStyles); export default styles;
Let´s move on with next component common text
Text.js
import React from 'react'; import PropTypes from 'prop-types'; import {Text, TextPropTypes} from 'react-native'; import {COLOR} from 'utils/styles'; import styles from './text.styles'; function mapStateAndPropsToStyle({size, color}) { const componentStyles = [styles.text]; if (size && styles[size]) { componentStyles.push(styles[size]); } if (color) { componentStyles.push({color}); } return componentStyles; } export default function CalcText({size, color, children, ...rest}) { return ( <Text {...rest} style={mapStateAndPropsToStyle({size, color})}> {children} </Text> ); } CalcText.propTypes = { ...TextPropTypes, size: PropTypes.string, }; CalcText.defaultProps = { size: 'md', color: COLOR.BLACK, };
Let’s summarize. We again make use of the function masStateAndPropsToStyle to calculate dynamic styles.
Besides that, everything else is self explanatory. One more thing we did is that we supplied defaultProps to have a default behavior in case the developer delivers a plain props object.
### Text.styles.js
import {StyleSheet} from 'react-native'; import {widthPercentageToDP as wp} from 'react-native-responsive-screen'; const baseStyles = { text: { fontFamily: 'Audiowide-Regular', }, xs: { fontSize: wp(2), }, sm: { fontSize: wp(4), }, md: { fontSize: wp(6), }, lg: { fontSize: wp(8), }, }; const styles = StyleSheet.create(baseStyles); export default styles;
Finally, let’s start with mainButton.js
This is the very same as our normal button but, a gradient version of it, Since gradient has no support, by default we are going to install another dependency.
'yarn add react-native-linear-gradient
Please do not forget to follow this tutorial for a good installation https://github.com/react-native-community/react-native-linear-gradient
For ‘iOS’, it is as simple as run ‘npx pod-install ios’ but for ‘android’ it is more complex. Remember that in this version of rn you are no longer required to add the pod line. rn will take care of it dynamically. However, if you are using a lower version please make sure you add the pod line in podfile.
Now,
### mainButton.js
import React, {useContext} from 'react'; import {TouchableOpacity} from 'react-native'; import PropTypes from 'prop-types'; import LinearGradient from 'react-native-linear-gradient'; import Text from 'components/text'; import ThemeContext from 'contexts/themes'; import styles from './mainButton.styles'; function mapStateAndPropsToStyles({style, linearGradientStyle}) { const componentStyles = { mainButton: [styles.mainButton], linearGradient: [styles.linearGradient], }; if (style) { if (Array.isArray(style)) { componentStyles.mainButton.push(...style); } else { componentStyles.mainButton.push(style); } } if (linearGradientStyle) { if (Array.isArray(linearGradientStyle)) { componentStyles.linearGradient.push(...linearGradientStyle); } else { componentStyles.linearGradient.push(linearGradientStyle); } } return componentStyles; } function drillProps({textColor}) { return { color: textColor, }; } function MainButton(props) { const [theme] = useContext(ThemeContext); const {mainButton: themeMainButtonProps = {}} = theme; const mixedProps = {...themeMainButtonProps, ...props}; const { colors, textColor, onPress, children, textProps = {}, style, } = mixedProps; const { mainButton: mainButtonStyle, linearGradient: linearGradientStyle, } = mapStateAndPropsToStyles({style}); return ( <TouchableOpacity style={mainButtonStyle} onPress={onPress}> <LinearGradient colors={colors} style={linearGradientStyle}> <Text {...drillProps({textColor})} {...textProps}> {children} </Text> </LinearGradient> </TouchableOpacity> ); } MainButton.propTypes = { onPress: PropTypes.func, textColor: PropTypes.string, textProps: PropTypes.object, colors: PropTypes.arrayOf(PropTypes.string), style: PropTypes.oneOfType([ PropTypes.number, PropTypes.object, PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.object), ]), linearGradientStyle: PropTypes.oneOfType([ PropTypes.number, PropTypes.object, PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.object), ]), }; export default MainButton;
Nothing else to explain ver implementation other than the components
### mainButton.styles.js
import {StyleSheet} from 'react-native'; const baseStyles = { mainButton: { flex: 1, }, linearGradient: { height: '100%', width: '100%', justifyContent: 'center', alignItems: 'center', }, }; const styles = StyleSheet.create(baseStyles); export default styles;
Now we are at the fun part. Let’s analyse how we are going to do the hard part. We are going to have a data structure like this:
{ left: '9', op: '*', right: '4324' }
A binary structure, if this was another languages other than js we could add an operator overload to achieve what we want, since operators are binary operators like 9 * 8, or 7 / 2
So we are going to always have a left value, our initial would be 0. If the user clicks something that is a number, we will be adding
entries to that number, because it is zero. Since it has no value we will replace it for the first digit. Notice that ‘.’counts as a new digit and cannot be set twice.
For example
If user presses 9, then 8, then ‘.’ ,and then 9 again.
We will be
* replacing 0 by 9
* adding 8
* adding .
* adding 9
* and ignoring ‘.’
<strong><em>However, we need to make sure we are writing on the right side. Are on the left side or on the right side???</em></strong>
To answer this question is going to be simple. We only need to check if the operator is set. We need to write on the right side or on the left operator.
So let’s start.
This is the react tree structure:
function debug() { return JSON.stringify({left, op, right}, null, '\t'); } const { mainScreen: mainScreenStyles, lowerContainer: lowerContainerStyles, } = mapStateAndPropsToStyles({ style, lowerContainerStyle, }); <View style={mainScreenStyles}> <View style={styles.upperContainer}> {/* <View> <Text>{debug()}</Text> </View> */} </View> <Text size="lg">{right || left}</Text> <View style={lowerContainerStyles}> <View style={styles.row}> <Button onPress={() => restart(0)}>C</Button> <Button onPress={() => handlePressSwitchSign()}>+/-</Button> <Button onPress={() => handlePressPercentage()}>%</Button> <Button onPress={() => handlePressOp('/')}>/</Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('7')}>7</Button> <Button onPress={() => handlePressDigit('8')}>8</Button> <Button onPress={() => handlePressDigit('9')}>9</Button> <Button onPress={() => handlePressOp('*')}>x</Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('4')}>4</Button> <Button onPress={() => handlePressDigit('5')}>5</Button> <Button onPress={() => handlePressDigit('6')}>6</Button> <Button onPress={() => handlePressOp('+')}> + </Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('1')}>1</Button> <Button onPress={() => handlePressDigit('2')}>2</Button> <Button onPress={() => handlePressDigit('3')}>3</Button> <Button onPress={() => handlePressOp('-')}> - </Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('0')}> 0 </Button> <Button onPress={() => handlePressDigit('.')} . </Button> <MainButton style={styles.mainButton} onPress={equal}> = </MainButton> </View> </View> </View>
We can uncomment the debug piece so all the time you will be able to see the behavior of our state <strong><em>which is skipped for brevity</em></strong>
So, this is what we will see at all times.
left: ‘0’,
op: null,
right: null,
lastOp: null,
Notice the <strong><em>lastOp</em></strong> function. This is important since we are going to use that to repeat operations if the user keeps pressing ‘=’
Let’s suppose the user clicks 5 then + then 5 then =, and from there =, =
That way the first operation performed + 5 will be added to the result every time.
### Important!!!
Since we are going to present all data in calculator to the user, we always need to convert our results to string. That way, any entry can be concatenated.
So, let’s start with the function of numbers and the dot:
### handlePressDigit Method
function handlePressDigit(digit) { const newState = {}; if (op) { if (right === '0' && digit === '0') { return; } const isDot = digit === '.'; if (isDot && right && right.indexOf('.') !== -1) { return; } newState.right = right === '0' || !right ? (isDot ? '0.' : digit) : `${right}${digit}`; } else { if (left === '0' && digit === '0') { return; } if (digit === '.' && left.indexOf('.') !== -1) { return; } newState.left = left === '0' || !left ? (isDot ? '0.' : digit) : `${left}${digit}`; } // mix between prevstate and new state setState((prevState) => ({ ...prevState, ...newState, })); }
We start with a new empty state, from where we are going to prepare. If left side is already taken, we know the op value otherwise it means that we are on the left side. If the digit is zero and we have zero or is a dot and we already have a dot we are going to escape. If not we have two courses of action:
if current side (left or right accordingly) is empty - then check if is dot - will be `0.` something else will replace the 0/null for the digit in question else will add at the end the new digit `${left}${digit}`;
So far so good. We are almost done. The other important thing will be the calculations. This is more complex, so pay attention. We are close to end.
Lets pass to operators
### handlePressOp Method
function handlePressOp(nextOp) { let nextState = {}; if (op && right) { const result = calc(left, op, right); nextState = { right: null, left: result, }; } setState((prevState) => ({ ...prevState, ...nextState, op: nextOp, })); }
Operation can be one of ‘*’ ‘/’ ‘+’ ‘-‘
If left op already exists, We will proceed to make the calculation between left , op and right, meaning the result. For example:
9 * 9, otherwise it will only register the nextOp to perform, for example if you have
* left = 90 and no op
* then press `*`
* then press `+`
Nothing will happen , only we are going to change ‘*’ with ‘+’ as next operation. Remember that when op is set, you can start typing and whatever you start typing is going to be redirected to right, and if by chance you try ‘=’ or ‘*’ or any operator again <strong><em>you will end up doing the calculation, then empty right side and pass the result to left side, and so on and so forth</em></strong>
Let’s pass to calc method
### Calc Method
function calc(left, op, right) { if (!left || !op || !right) { return; } const maxDecimals = (left.split('.')[1]?.length || 0) + (right?.split('.')[1]?.length || 0); switch (op) { case '*': { return getRidOfRightZeroes(multiply(left, right, maxDecimals)); } case '/': { return getRidOfRightZeroes(divide(left, right, maxDecimals)); } case '+': { return getRidOfRightZeroes(add(left, right, maxDecimals)); } case '-': { return C(substract(left, right, maxDecimals)); } default: throw new Error(`${op} Not implemented`); } }
First we are going to check if we have everything we need to perform a calc which is ‘left’ , ‘op’ and ‘right’.
After that, since we are managing decimal operations we need to make sure we retrieve the right number of decimals. For this we are going to emulate our computer’s calculator behavior. Just Add both decimal numbers, left and right.
If we have
* 98089.9 * 89.10
then our max number of decimals is 3
Then we are going to check if operation is registered. If not, we throw an error and finally just get rid of the extra zeroes and perform the operation <strong><em>Remember what we discussed. All operations will convert to number perform operation’s and then get back to strings</em></strong>
Let’s say we have 59.09000
In that case we only care about 59.09 right? That is what the getRidOfRightZeroes does. Let’s take a closer look .
### getRidOfRightZeroes Method
function getRidOfRightZeroes(result) { const [, decimals] = result.split('.'); if (!decimals) { return result; } let charsToRemove = 0; const len = result.length - 1; for (let i = len; i > -1; i--) { const lastChar = result[i]; if (lastChar === '0') { charsToRemove++; } else if (lastChar === '.') { charsToRemove++; break; } else { break; } } if (!charsToRemove) { return result; } return result.slice(0, -charsToRemove); }
We are going to look for decimals. If not then return the same result.
But, if there are decimals, we sit at the right side and start moving to the left side until we hit a number other than 0 or .
If we have `89.00` then it will become `89`
Almost done. Let’s look at the operations implementation.
### Operations Methods
function divide(left, right, decimals = 0) { if (left === '0' || right === '0') { return '0'; } return (+left / +right).toFixed(decimals); } function multiply(left, right, decimals = 0) { if (left === '0' || right === '0') { return '0'; } return (+left * +right).toFixed(decimals); } function add(left, right, decimals = 0) { return (+left + +right).toFixed(decimals); } function substract(left, right, decimals = 0) { return (+left - +right).toFixed(decimals); }
Just one thing to explain here: if we use the operator ‘+’ before any variable, it will try to convert to a number even when their is a negative number like ‘-89’ will be converted to ‘-89’
Last but not least, let’s examine the equal functionality.
function equal() { let nextState = null; if (op && right) { nextState = { left: calc(left, op, right), right: null, lastOp: { op, right, }, }; } else if (lastOp) { const {op, right} = lastOp; nextState = { left: calc(left, op, right), right: null, }; } else if (op) { nextState = { left: calc(left, op, left), right: null, lastOp: { op, right: left, }, }; } if (!nextState) { return; } setState((prevState) => ({ ...prevState, ...nextState, })); }
- First we are creating a ref to nextState
- If there is an op to perform, it is done by checking the presence of ‘right’ and ‘op’ same thing as operator handling. If those elements are present we proceed to calc and assign that to ‘left’ side and empty ‘right’ side
- But if there is a ‘lastOp’ registered, we go with that one.
- If their are none but there is a left value and an ‘op’ then we finally go with duplicating the left value to the right value and calculating
Full code for Main.js
import React, {useState, useEffect, useContext} from 'react'; import {View, TouchableOpacity} from 'react-native'; import PropTypes from 'prop-types'; import Button from 'components/calcButton'; import MainButton from 'components/mainButton'; import Text from 'components/text'; import {COLOR} from 'utils/styles'; import LinearGradient from 'react-native-linear-gradient'; import ThemeContext from 'contexts/themes'; import {THEME} from 'utils/themes'; import styles from './main.styles'; const INITIAL_STATE = { history: [], left: '0', op: null, right: null, lastOp: null, }; function mapStateAndPropsToStyles({isDarkish, style, lowerContainerStyle}) { const componentStyles = { lowerContainer: [styles.lowerContainer], mainScreen: [styles.mainScreen], }; if (isDarkish) { componentStyles.lowerContainer.push(styles.darkish); } if (lowerContainerStyle) { if (Array.isArray) { componentStyles.lowerContainer.push(...style); } else { componentStyles.lowerContainer.push(style); } } if (style) { if (Array.isArray) { componentStyles.mainScreen.push(...style); } else { componentStyles.mainScreen.push(style); } } return componentStyles; } function MainScreen(props) { const [theme, setTheme] = useContext(ThemeContext); const {mainScreen: themeMainScreenProps = {}} = theme; const [{left, op, right, history, lastOp}, setState] = useState( INITIAL_STATE, ); const mixedProps = {...themeMainScreenProps, ...props}; const {isDarkish, style, lowerContainerStyle} = mixedProps; function calc(left, op, right) { if (!left || !op || !right) { return; } const maxDecimals = (left.split('.')[1]?.length || 0) + (right?.split('.')[1]?.length || 0); switch (op) { case '*': { return getRidOfRightZeroes(multiply(left, right, maxDecimals)); } case '/': { return getRidOfRightZeroes(divide(left, right, maxDecimals)); } case '+': { return getRidOfRightZeroes(add(left, right, maxDecimals)); } case '-': { return getRidOfRightZeroes(substract(left, right, maxDecimals)); } default: throw new Error(`${op} Not implemented`); } } function handlePressDigit(digit) { const newState = {}; const isDot = digit === '.'; if (op) { if (right === '0' && digit === '0') { return; } if (isDot && right && right.indexOf('.') !== -1) { return; } newState.right = right === '0' || !right ? (isDot ? '0.' : digit) : `${right}${digit}`; } else { if (left === '0' && digit === '0') { return; } if (digit === '.' && left.indexOf('.') !== -1) { return; } newState.left = left === '0' || !left ? (isDot ? '0.' : digit) : `${left}${digit}`; } setState((prevState) => ({ ...prevState, ...newState, })); } function getRidOfRightZeroes(result) { const [, decimals] = result.split('.'); if (!decimals) { return result; } let charsToRemove = 0; const len = result.length - 1; for (let i = len; i > -1; i--) { const lastChar = result[i]; if (lastChar === '0') { charsToRemove++; } else if (lastChar === '.') { charsToRemove++; break; } else { break; } } if (!charsToRemove) { return result; } return result.slice(0, -charsToRemove); } function divide(left, right, decimals = 0) { if (left === '0' || right === '0') { return '0'; } return (+left / +right).toFixed(decimals); } function multiply(left, right, decimals = 0) { if (left === '0' || right === '0') { return '0'; } return (+left * +right).toFixed(decimals); } function add(left, right, decimals = 0) { return (+left + +right).toFixed(decimals); } function substract(left, right, decimals = 0) { return (+left - +right).toFixed(decimals); } function restart(initialValue = '0') { setState({ ...INITIAL_STATE, left: '' + initialValue, }); } function handlePressOp(nextOp) { let nextState = {}; if (op && right) { const result = calc(left, op, right); nextState = { right: null, left: result, history: [1], }; } setState((prevState) => ({ ...prevState, ...nextState, op: nextOp, })); } function handlePressSwitchSign() { const nextState = {}; if (op) { if (right === '0') { return; } nextState.right = right.startsWith('-') ? right.slice(1) : '-' + right; } else { if (left === '0') { return; } nextState.left = left.startsWith('-') ? left.slice(1) : '-' + left; } setState((prevState) => ({ ...prevState, ...nextState, })); } function handlePressPercentage() { const nextState = {}; if (op && right) { nextState.right = `${+right / 100}`; } else { nextState.left = `${+left / 100}`; } setState((prevState) => ({ ...prevState, ...nextState, })); } function equal() { let nextState = null; if (op && right) { nextState = { left: calc(left, op, right), right: null, lastOp: { op, right, }, }; } else if (lastOp) { const {op, right} = lastOp; nextState = { left: calc(left, op, right), right: null, }; } else if (op) { nextState = { left: calc(left, op, left), right: null, lastOp: { op, right: left, }, }; } if (!nextState) { return; } setState((prevState) => ({ ...prevState, ...nextState, })); } function debug() { return JSON.stringify({left, op, right}, null, '\t'); } function handlePressPinkishTheme() { setTheme(THEME.PINKISH); } function handlePressGreenishTheme() { setTheme(THEME.GREENISH); } const { mainScreen: mainScreenStyles, lowerContainer: lowerContainerStyles, adornment: adornmentStyles, } = mapStateAndPropsToStyles({ isDarkish, style, lowerContainerStyle, }); return ( <View style={mainScreenStyles}> <View style={styles.themesContainer}> <TouchableOpacity onPress={handlePressPinkishTheme}> <LinearGradient style={styles.theme} colors={COLOR.PINKISH} /> </TouchableOpacity> <TouchableOpacity onPress={handlePressGreenishTheme}> <LinearGradient style={styles.theme} colors={COLOR.GREENISH} /> </TouchableOpacity> </View> <View style={styles.upperContainer}> {/* <View> <Text>{debug()}</Text> </View> */} <View style={styles.historyContainer}> {history.map((result, i) => { return ( <Text size="sm" key={`${i}-${result}`} onPress={() => restart(result)}> {result} </Text> ); })} </View> <Text size="lg">{right || left}</Text> <View style={adornmentStyles} /> </View> <View style={lowerContainerStyles}> <View style={styles.row}> <Button onPress={() => restart(0)}>C</Button> <Button onPress={() => handlePressSwitchSign()}>+/-</Button> <Button onPress={() => handlePressPercentage()}>%</Button> <Button onPress={() => handlePressOp('/')}>/</Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('7')}>7</Button> <Button onPress={() => handlePressDigit('8')}>8</Button> <Button onPress={() => handlePressDigit('9')}>9</Button> <Button onPress={() => handlePressOp('*')}>x</Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('4')}>4</Button> <Button onPress={() => handlePressDigit('5')}>5</Button> <Button onPress={() => handlePressDigit('6')}>6</Button> <Button onPress={() => handlePressOp('+')}>+</Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('1')}>1</Button> <Button onPress={() => handlePressDigit('2')}>2</Button> <Button onPress={() => handlePressDigit('3')}>3</Button> <Button onPress={() => handlePressOp('-')}>-</Button> </View> <View style={styles.row}> <Button onPress={() => handlePressDigit('0')}>0</Button> <Button onPress={() => handlePressDigit('.')}>.</Button> <MainButton style={styles.mainButton} onPress={equal}> = </MainButton> </View> </View> </View> ); } MainScreen.propTypes = { style: PropTypes.oneOfType([ PropTypes.number, PropTypes.object, PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.object), ]), lowerContainerStyle: PropTypes.oneOfType([ PropTypes.number, PropTypes.object, PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.object), ]), }; export default MainScreen;
By – Cesar Rodriguez Chavez, Senior Software Engineer, DigitalOnUs