はじめに
この記事は**「Reactのチュートリアルを終えて、何かを作ってみたい」**という読者を想定しています。
分からない部分が出た場合はReactの公式ドキュメントに立ち返りましょう。
material-uiの公式ドキュメントも要チェック!
※更新 2020/07/23 桁数調整を追加. 変数名を修正. gifを追加
※初稿 2020/04/05
作ったもの
iPhoneに入っている電卓アプリを参考にして作りました。
戦略
ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します。
component、containerの洗い出し
component
- ボタン: 押したら何かしらの動作を起こすコンポーネント

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

container
- calculator: ボタンとディスプレイを集結させたもの。今回はこれ自体がアプリとなる。
stateの洗い出し
iPhoneの電卓アプリをいじってみて、どんなstateが必要かを考えます。
はじめにざっくりと書き出して、実際には実装しながら追加していきました。
-
firstNumber
:A + B
のA
にあたる数の文字列 -
secondNumber
:A + B
のB
にあたる数の文字列 -
operator
: add(加算), subtract(減算), divide(除算), multiply(乗算)。operatorが指定されていないときはfalse。 -
isAllClear
: clearボタンがAC / Cどちらかを判別するboolean。 -
isDicimalInput
: .ボタンが押されて小数点以下入力になっているか判別するboolean。 -
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
ファイル構成

コンポーネントの実装
Display
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
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>
);
}
ポイント
ボタンコンポーネントは親から
- 背景色
- クリックしたときに実行する関数
- ボタンの文字
を受け取ります。
親コンポーネントで<MyButton onClick={somthing}>
としても関数は実行されません。
子のButtonのonClickに接続する必要があります。
コンテナの実装
長いコードなのでコメントアウトで説明します。
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
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 {
text-align: center;
}
.container {
margin: 80px auto 80px auto;
padding: 20px;
max-width: 300px;
background-color: #80deea;
}
以上で完成です!
おまけ
こちらの記事を参考に、gh-pagesでサイトを公開しました。
https://shintaro-hirose.github.io/react-calculator/
今後もReactで簡単なアプリを作っていきます!