【React + Material-UI】電卓アプリを作ろう！【2020年4月版】

# はじめに

この記事は**「Reactのチュートリアルを終えて、何かを作ってみたい」**という読者を想定しています。

分からない部分が出た場合は[Reactの公式ドキュメント](https://ja.reactjs.org/)に立ち返りましょう。

material-uiの[公式ドキュメント](https://material-ui.com/)も要チェック!

※更新 2020/07/23 桁数調整を追加. 変数名を修正. gifを追加
※初稿 2020/04/05

# 作ったもの

- [React Calculator デモページ](https://shintaro-hirose.github.io/react-calculator/)
- [ソースコード](https://github.com/shintaro-hirose/react-calculator)

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376127/bbf8c766-986b-c7ea-f185-5c49af7354cc.gif)

iPhoneに入っている電卓アプリを参考にして作りました。

# 戦略

Reactコンポーネントには**Material-UIを**使い、保持するstateをReactの**useState**で実装します。

# component、containerの洗い出し

## component

- ボタン: 押したら何かしらの動作を起こすコンポーネント
<img width="75" alt="スクリーンショット 2020-04-05 0.20.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376127/3315c623-0e51-28f3-186e-12db78a3d7d5.png">


- ディスプレイ: 上部にある、数字を表示するためのコンポーネント

<img width="317" alt="スクリーンショット 2020-04-05 0.20.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376127/0b817643-7970-5fa2-08e3-4e5290d88434.png">


## container

- calculator: ボタンとディスプレイを集結させたもの。今回はこれ自体がアプリとなる。 

# stateの洗い出し

iPhoneの電卓アプリをいじってみて、どんなstateが必要かを考えます。
はじめにざっくりと書き出して、実際には実装しながら追加していきました。

1. `firstNumber`: `A + B` の`A`にあたる数の文字列
2. `secondNumber`: `A + B` の`B`にあたる数の文字列
3. `operator`: add（加算）, subtract（減算）, divide（除算）, multiply（乗算）。operatorが指定されていないときはfalse。
4. `isAllClear`: clearボタンがAC / Cどちらかを判別するboolean。
5. `isDicimalInput`: .ボタンが押されて小数点以下入力になっているか判別するboolean。
6. `isAnswerDisplay`: =ボタンが押された直後には特殊な挙動をするので、判別するbooleanを用意しておく。

たとえば、 `5 * 7 =` と入力した場合、
`firstNumber`は5, `secondNumber`は7, `operator`は"multiply", `isAnswerDisplay`はtrueとなる。

# インストール

## create react app

```
npx create-react-app react-calculator
cd react-calculator
```

## @material-ui/core

```
// npm を使う場合
npm install @material-ui/core

// yarn を使う場合
yarn add @material-ui/core
```

### バージョン情報

- react: 16.13.1
- @material-ui/core: 4.9.8

# ファイル構成

<img width="400" alt="スクリーンショット 2020-04-04 23.04.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376127/5de318fc-730f-4364-ceee-0c95bf6689e6.png">

# コンポーネントの実装

## Display

```jsx:Display.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';

const useStyles = makeStyles((theme) => ({
    box: {
        fontSize: "60px",
        width: "100%",
        margin: "0 auto",
    },
  }));

export default function Display(props) {
  const classes = useStyles();
  
  return (
      <div>
       <Box className={classes.box}>
            {props.children}
       </Box>
      </div>
  );
}
```

### ポイント
`props.children`とすることで、`<Display></Display>`ではさんだものを表示できます。

## MyButton

```jsx:MyButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function MyButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(1),
      backgroundColor: props.backgroundColor
    },
  }));
  const classes = useStyles();


  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}
```

### ポイント

ボタンコンポーネントは親から

1. 背景色
2. クリックしたときに実行する関数
3. ボタンの文字

を受け取ります。

親コンポーネントで`<MyButton onClick={somthing}>`としても関数は実行されません。
**子のButtonのonClickに接続する必要があります**。

# コンテナの実装

長いコードなのでコメントアウトで説明します。

```jsx:Calculator.js
import React, {useState} from 'react';

//components
import MyButton from '../components/MyButton';
import Display from '../components/Display';

//material-ui
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

function Calculator(){
    //保持したいstates
    const [firstNumber, setFirstNumber] = useState("0");
    const [secondNumber, setSecondNumber] = useState("0");
    const [operator, setOperator] = useState(false);
    const [isAllClear, setIsAllClear] = useState(true);
    const [isDicimalInput, setIsDicimalInput] = useState(false);
    const [isAnswerDisplay, setIsAnswerDisplay] = useState(false);

    //数字ボタンを押したとき
    const handleNumberClick = (num) => {
        //=ボタンを押した直後に数字を押した場合
        if(isAnswerDisplay){
            setFirstNumber(num);
            setSecondNumber("0");
            setOperator(false);
            setIsAnswerDisplay(false);
            setIsDicimalInput(false);
        } else {
           //演算子が押されていないときはfirstNumberを更新する
           if(!operator){
                if(isDicimalInput){
                    setFirstNumber(String(firstNumber) + String(num))
                } else {
                    setFirstNumber(String(Number(firstNumber)*10+num));
                }
            //演算子が押されているときはsecondNumberを更新する
            } else {
                if(isDicimalInput){
                    setSecondNumber(String(secondNumber) + String(num))
                } else {
                    setSecondNumber(String(Number(secondNumber)*10+num));
                }
            }
        }
        //0以外が押されたときはACをCに変える
        if(num !== 0){
            setIsAllClear(false);
        }
    }

    //00ボタンを押したとき
    const handleDoubleZeroClick = () => {
        if(!operator){
            if(isDicimalInput){
                setFirstNumber(String(firstNumber) + "00")
            } else {
                setFirstNumber(String(Number(firstNumber)*100));
            }
        } else {
            if(isDicimalInput){
                setSecondNumber(String(secondNumber) + "00")
            } else {
                setSecondNumber(String(Number(secondNumber)*100));
            }
        }
    }

    //AC/Cボタンを押したとき
    const handleClearClick = () => {
        setIsDicimalInput(false);
        //ACのときはすべてリセット
        if(isAllClear){
            setFirstNumber("0");
            setOperator(false);
            setIsAnswerDisplay(false)
        //CのときはsecondNumberを"0"にする
        } else {
            setIsAllClear(true);
            if(isAnswerDisplay){
                setFirstNumber("0")
            } else{
                if(!operator){
                    setFirstNumber("0");
                } else {
                    setSecondNumber("0");
                }
            }
        }
    }

    // -/+ボタンを押したとき
    const handleMinusPlusClick = () => {
        if(isAnswerDisplay){
            setFirstNumber(-1 * Number(firstNumber));
        } else {
            if(!operator){
                setFirstNumber(-1 * Number(firstNumber));
            } else {
                setSecondNumber(-1 * Number(secondNumber));
            }
        }
        
    }

    // %ボタンを押したとき
    const handlePercentageClick = () => {
        if(isAnswerDisplay){
            setFirstNumber(Number(firstNumber) * 0.01);
        } else {
            if(!operator){
                setFirstNumber(Number(firstNumber) * 0.01);
            } else {
                setSecondNumber(Number(secondNumber) * 0.01);
            }
        }
    }

    //演算子を押したとき
    //引数はadd subtract divide multiplyのどれか
    const handleOperatorClick = (a) => {
        if(isAnswerDisplay){
            setIsAnswerDisplay(false);
        } else {
            if(secondNumber !== "0"){
                if(operator === "add"){
                    setFirstNumber(Number(firstNumber) + Number(secondNumber))
                } else if(operator === "subtract"){
                    setFirstNumber(Number(firstNumber) - Number(secondNumber))
                } else if(operator === "divide"){
                    setFirstNumber(Number(firstNumber) / Number(secondNumber))
                } else if(operator === "multiply"){
                    setFirstNumber(Number(firstNumber) * Number(secondNumber))
                }
            }
        }
        setIsDicimalInput(false);
        setOperator(a);
        setSecondNumber("0")
    }

    // .ボタンを押したとき
    const handleDicimalPointClick = () => {
        if(isDicimalInput) return;

        setIsDicimalInput(true);
        if(!operator){
            setFirstNumber(String(firstNumber) + '.')
        } else {
            setSecondNumber(String(secondNumber) + '.')
        }
    }

    // =ボタンを押したとき
    const handleAnswerClick = () => {
        setIsDicimalInput(false);
        setIsAnswerDisplay(true);
        if(operator === "add") {
            if(secondNumber === "0"){
                //演算子が指定されたあと、secondNumberが入力されずに=が押された場合
                //例えば、5 + = と入力した場合、10と表示する。
                setSecondNumber(firstNumber)
                setFirstNumber(Number(firstNumber) + Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) + Number(secondNumber))
            }
        } else if(operator === "subtract") {
            if(secondNumber === "0"){
                setSecondNumber(Number(firstNumber))
                setFirstNumber(Number(firstNumber) - Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) - Number(secondNumber))
            }
        } else if(operator === "divide") {
            if(secondNumber === "0"){
                setSecondNumber(Number(firstNumber))
                setFirstNumber(Number(firstNumber) / Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) / Number(secondNumber))
            }
        } else if(operator === "multiply"){
            if(secondNumber === "0"){
                setSecondNumber(Number(firstNumber))
                setFirstNumber(Number(firstNumber) * Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) * Number(secondNumber))
            }
        }
    }

    //大きい桁数や小数点以下桁数の調整
    const fixDigits = (num) => {
        if((num) >= 10e8){
            return num.toExponential(2);
        } else {
            return Math.round(num*1000000)/1000000;
        }
    }
    //表示する数字
    const displayMarkup = (!isAnswerDisplay && operator ) ? fixDigits(Number(secondNumber)) : fixDigits(Number(firstNumber));
    const clearButtonMarkup = isAllClear ? "AC" : "C";

    return(
        <div>
            <Typography variant="h5">
                React Calculator
            </Typography>
            <Display>
                {displayMarkup}
            </Display>
            //material-uiのBoxはflexを使って好きなように並べられる。公式ドキュメント参照。
            <Box display="flex" flexDirection="column">
                <Box display="flex" justifyContent="center">
                   //ボタンの中に表示したいものをMyButtonではさむ
                   //MyButtonのpropsに背景色、onClick関数を渡す
                    <MyButton backgroundColor="gray" onClick={handleClearClick}>
                        <Typography variant="h5">{clearButtonMarkup}</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handleMinusPlusClick}>
                        <Typography variant="h5">-/+</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handlePercentageClick}>
                        <Typography variant="h5">%</Typography>
                    </MyButton>
                    //onClickに引数を渡したい場合はこのように記述する
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("divide")}>
                        <Typography variant="h5" >÷</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center" >
                    <MyButton onClick={() => handleNumberClick(7)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">7</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(8)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">8</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(9)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">9</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("multiply")}>
                        <Typography variant="h5" >×</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(4)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">4</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(5)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">5</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(6)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">6</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("subtract")}>
                        <Typography variant="h5" >-</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(1)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">1</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(2)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">2</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(3)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">3</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("add")}>
                        <Typography variant="h5" >+</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(0)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">0</Typography>
                    </MyButton>
                    <MyButton onClick={handleDoubleZeroClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5">00</Typography>
                    </MyButton>
                    <MyButton onClick={handleDicimalPointClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5" >.</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={handleAnswerClick}>
                        <Typography variant="h5" >=</Typography>
                    </MyButton>
                </Box>
            </Box>
        </div>
    )
};

export default Calculator;
```

### ポイント

- `handleNumberClick`などの引数がある関数は、`onClick={() => somthing(引数)}`のように記述します
- stateはひとつにまとめても良いが、どこで何を使うかが決まってないうちは独立したstateにしておいて後からまとめよう。

## App.js

```jsx:App.js
import React from 'react';
import './App.css';

import Calculator from './containers/Calculator'

function App() {
  return (
    <div className="App">
      <div className="container">
        <Calculator />
      </div>
    </div>
  );
}

export default App;
```

## App.css

```App.css
.App {
  text-align: center;
}
.container {
  margin: 80px auto 80px auto;
  padding: 20px;
  max-width: 300px;
  background-color: #80deea;
}
```

以上で完成です！

# おまけ

[こちらの記事](https://qiita.com/EisKern/items/15dcf7864fa49df8f247)を参考に、gh-pagesでサイトを公開しました。
https://shintaro-hirose.github.io/react-calculator/

今後もReactで簡単なアプリを作っていきます！