JavaScript
React
redux

『React』+『Redux』でiPhoneに入ってるような電卓を作ってみよう!


はじめに

前回の記事で『Todo』の実装を終えて、次に何を作ろうか悩んでいた時にこのような記事を見つけました。

今回はこの記事を参考にし、電卓アプリケーションを作ることによりReactとReduxの理解を深めていきたいと思います!

前回の記事


完成品

ezgif.com-optimize (5).gif


準備


何はともあれcreate-react-appから

terminalで以下のコマンドを入力しましょう!

$ create-react-app calculator

$ cd calculator
$ yarn start

これで、以下のようにReactを用いたWebアプリケーションを作成する準備ができました!

スクリーンショット 2019-05-15 10.01.44.png

create-react-appがわからない人はこちらの記事へ


Reduxのインストール

さらに今回はReduxを用いるので、以下のコマンドを入力してReduxのパッケージをインストールしましょう!

yarn add react-redux redux

これで準備完了です!


早速実装してみよう!


見た目から入る

まずは電卓らしい見た目を作っていきましょう!以下の3つのComponentを作成します

『0~9の数字ボタン(NumBtn.js)』


src/components/NumBtn.js

import React from 'react';

const NumBtn = ({n}) => <button>{n}</button>;

export default NumBtn;


『+, -, ×, ÷の計算ボタン(OperatorBtn.js)』


src/components/OperatorBtn.js

import React from 'react';

const OperatorBtn = ({o}) => <button>{o}</button>;

export default OperatorBtn;


『計算結果を表示する(Result.js)』


src/components/Result.js

import React from 'react';

const Result = ({result}) => <div className="resultValue">{result}</div>;

export default Result;


この3つのComponentをApp.jsにimportしましょう!


src/App.js

import React from 'react';

import './App.css';
import NumBtn from './components/NumBtn';
import OperatorBtn from './components/OperatorBtn';
import Result from './components/Result';

function App() {
return (
<React.Fragment>
<div className="result">
<Result result={'計算結果'} />
</div>
<div className="wrapper">
<div className="number">
<div className="numUpper">
<NumBtn n={7} />
<NumBtn n={8} />
<NumBtn n={9} />
</div>
<div className="numMiddle">
<NumBtn n={4} />
<NumBtn n={5} />
<NumBtn n={6} />
</div>
<div className="numLower">
<NumBtn n={1} />
<NumBtn n={2} />
<NumBtn n={3} />
</div>
<div className="zero">
<NumBtn n={0} />
<span className="allClear">
<OperatorBtn o={'AC'} />
</span>
<span className="equal">
<OperatorBtn o={'='} />
</span>
</div>
</div>
<div className="operator">
<OperatorBtn o={'÷'} />
<OperatorBtn o={'×'} />
<OperatorBtn o={'-'} />
<OperatorBtn o={'+'} />
</div>
</div>
</React.Fragment>
);
}

export default App;


そしてApp.cssにてスタイルを変更すると


src/App.css

  #root {

width:200px;
margin: 40px auto;
}

.result {
height: 40px;
background: black;
}

.resultValue {
text-align: end;
height: 100%;
font-size: 30px;
color: white;
padding-right: 15px;
}

.wrapper {
display: flex;
justify-content: center;
background: black;
}

button {
border-radius: 50%;
width: 40px;
height: 40px;
margin: 5px;
background: #555555;
border: none;
color: white;
font-size: 25px;
padding: 0;
}

button:hover {
background: #999999;
}

button:focus {
outline: none;
}

.operator button {
background: orange;
}

.operator button:hover{
background: #FFC7AF;
}

.operator button:focus {
background: white;
color: orange;
}

.operator {
display: flex;
flex-direction: column;
}


出力画面がこのようになります。

スクリーンショット 2019-05-15 11.01.46.png

App.cssを変更して、ご自身の好きなデザインにするのも面白いかもしれませんね!

これにて見た目は完成です!


中身を実装しよう


Action

Componentを押された時に発行されるActionの定義を行なっていきましょう!ここでは


  • 『数字』が押された時

  • 『+、-、×、÷』が押された時

  • 『=』が押された時

  • 『AC』が押された時

のActionの定義を行なっていきたいと思います。

まずは、Actionのtypeの定義から


src/utils/actionTypes.js

export const INPUT_NUMBER = 'INPUT_NUMBER';

export const PLUS = 'PLUS';
export const MINUS = 'MINUS';
export const MULTIPLY = 'MULTIPLY';
export const DIVIDE = 'DIVIDE';

export const EQUAL = 'EQUAL';
export const CLEAR = 'CLEAR';


次にActionクリエイターを作成します。


src/actions/index

import * as actionTypes from '../utils/actionTypes';

export const onNumClick = number => ({
types: actionTypes.INPUT_NUMBER,
number,
});

export const onPlusClick = () => ({
types: actionTypes.PLUS,
});

export const onMinusClick = () => ({
types: actionTypes.MINUS,
});

export const onMultiplyClick = () => ({
types: actionTypes.MULTIPLY,
});

export const onDivideClick = () => ({
types: actionTypes.DIVIDE,
});

export const onEqualClick = () => ({
types: actionTypes.EQUAL,
});

export const onClearClick = () => ({
types: actionTypes.CLEAR,
});



Reduer

発行されたActionにより、どのように状態を遷移させるのかを記述するのがReducerです。電卓を作るのに必要な以下の5つの状態を遷移させます。


  • inputValue(入力された値)

  • operator(演算子)

  • resultValue(計算結果)

  • calculate(計算を行うかの判断)

  • showingResult(結果を表示するかの判断)

case actionTypes.EQUAL:では直前に押された演算子をoperatorに保存しておくことで、どのような演算を行うのかを判断しています。

calculateは演算子ボタンを押した時に演算を行うかどうか判断するためのものです。これがないと、GIFのように最初の状態から『8-5』を計算したいときに、『8-』を押した際に『0-8』が計算され、その後『-8-5』が計算されるので、結果が『-13』となってしまいます。

ezgif.com-optimize (6).gif


src/reducers/calculator.js

import * as actionTypes from '../utils/actionTypes';

const initialAppState = {
inputValue: 0,
operator: '',
resultValue: 0,
calculate: false,
showingResult: false,
};

const calculator = (state = initialAppState, action) => {
switch (action.type) {
case actionTypes.INPUT_NUMBER:
return {
...state,
inputValue: state.inputValue * 10 + action.number,
showingResult: false,
};

case actionTypes.PLUS:
if (state.calculate === true) {
return {
...state,
inputValue: 0,
operator: '+',
resultValue: state.resultValue + state.inputValue,
showingResult: true,
};
} else {
return {
...state,
inputValue: 0,
operator: '+',
calculate: true,
resultValue: state.inputValue,
showingResult: true,
};
}

case actionTypes.MINUS:
if (state.calculate === true) {
return {
...state,
inputValue: 0,
operator: '-',
resultValue: state.resultValue - state.inputValue,
showingResult: true,
};
} else {
return {
...state,
inputValue: 0,
operator: '-',
calculate: true,
resultValue: state.inputValue,
showingResult: true,
};
}

case actionTypes.MULTIPLY:
if (state.calculate === true) {
return {
...state,
inputValue: 0,
operator: '*',
resultValue: state.resultValue * state.inputValue,
showingResult: true,
};
} else {
return {
...state,
inputValue: 0,
operator: '*',
calculate: true,
resultValue: state.inputValue,
showingResult: true,
};
}

case actionTypes.DIVIDE:
if (state.calculate === true) {
return {
...state,
inputValue: 0,
operator: '/',
resultValue: state.resultValue / state.inputValue,
showingResult: true,
};
} else {
return {
...state,
inputValue: 0,
operator: '/',
calculate: true,
resultValue: state.inputValue,
showingResult: true,
};
}

case actionTypes.CLEAR:
return {
inputValue: 0,
operator: '',
calculate: false,
resultValue: 0,
showingResult: false,
};
case actionTypes.EQUAL:
switch (state.operator) {
case '+':
return {
inputValue: state.resultValue + state.inputValue,
operator: '',
calculate: false,
resultValue: state.resultValue + state.inputValue,
showingResult: true,
};
case '-':
return {
inputValue: state.resultValue - state.inputValue,
operator: '',
calculate: false,
resultValue: state.resultValue - state.inputValue,
showingResult: true,
};
case '*':
return {
inputValue: state.resultValue * state.inputValue,
operator: '',
calculate: false,
resultValue: state.resultValue * state.inputValue,
showingResult: true,
};
case '/':
return {
inputValue: state.resultValue / state.inputValue,
operator: '',
calculate: false,
resultValue: state.resultValue / state.inputValue,
showingResult: true,
};
default:
return state;
}

default:
return state;
}
};

export default calculator;


今回は1つのReducerしか作っていませんが、以下のようにconbineReducersを用いてReducerを結合させます。


src/reducers/index.js

import {combineReducers} from 'redux';

import calculator from './calculator';

const reducer = combineReducers({
calculator,
});

export default reducer;



Store

次は先ほど作成したReducerをもとに状態を保管するStoreを作りましょう。これによって、Reducerによって新しく作成された状態(Store)をAppコンポーネントに渡すことができます。


src/index.js

import React from 'react';

import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {createStore} from 'redux';
import {Provider} from 'react-redux';

import reducer from './reducers';

const store = createStore(reducer);

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);



ReactとReduxを結合させる

ここではApp.jsmapStateToPropsmapDispatchToPropsを追加し、connectで結合させましょう!

mapStateToProps

Storeが持つ状態(state)をどのようにpropsに混ぜ込むかを決める

mapDispatchToProps

Reducer にアクションを通知する関数dispatchをどのようにpropsに混ぜ込むかを決める

bindActionCreators()

Action を追加するたびにconnectに追加するのは大変なのでbindActionCreators を使って自動的にマッピングする


src/App.js

import React, {Component} from 'react';

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import './App.css';

import * as actions from './actions';
import NumBtn from './components/NumBtn';
import OperatorBtn from './components/OperatorBtn';
import Result from './components/Result';

class App extends Component {
render() {
const {calculator, actions} = this.props;
console.log(calculator);
console.log(actions);

return (
<React.Fragment>
<div className="result">
<Result
result={
calculator.showingResult
? calculator.resultValue
: calculator.inputValue
}
/>
</div>
<div className="wrapper">
<div className="number">
<div className="numUpper">
<NumBtn n={7} onClick={() => actions.onNumClick(7)} />
<NumBtn n={8} onClick={() => actions.onNumClick(8)} />
<NumBtn n={9} onClick={() => actions.onNumClick(9)} />
</div>
<div className="numMiddle">
<NumBtn n={4} onClick={() => actions.onNumClick(4)} />
<NumBtn n={5} onClick={() => actions.onNumClick(5)} />
<NumBtn n={6} onClick={() => actions.onNumClick(6)} />
</div>
<div className="numLower">
<NumBtn n={1} onClick={() => actions.onNumClick(1)} />
<NumBtn n={2} onClick={() => actions.onNumClick(2)} />
<NumBtn n={3} onClick={() => actions.onNumClick(3)} />
</div>
<div className="zero">
<NumBtn n={0} />
<span className="allClear">
<OperatorBtn o={'AC'} onClick={() => actions.onClearClick()} />
</span>
<span className="equal">
<OperatorBtn o={'='} onClick={() => actions.onEqualClick()} />
</span>
</div>
</div>
<div className="operator">
<OperatorBtn o={'÷'} onClick={() => actions.onDevideClick()} />
<OperatorBtn o={'×'} onClick={() => actions.onMultiplyClick()} />
<OperatorBtn o={'-'} onClick={() => actions.onMinusClick()} />
<OperatorBtn o={'+'} onClick={() => actions.onPlusClick()} />
</div>
</div>
</React.Fragment>
);
}
}

const mapStateToProps = state => ({
calculator: state.calculator,
});

const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators(actions, dispatch),
};
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(App);


最後にComponentも変更します。


src/components/NumBtn

import React from 'react';

const NumBtn = ({n, onClick}) => <button onClick={onClick}>{n}</button>;

export default NumBtn;



src/components/OperatorBtn.js

import React from 'react';

const OperatorBtn = ({o, onClick}) => <button onClick={onClick}>{o}</button>;

export default OperatorBtn;


以上で完成です。

電卓としての機能はまだ不十分なところもございますので、この記事を参考にしつつ新しい機能をつけていただけたら幸いです!


リファレンス