#はじめに
前回の記事で『Todo』の実装を終えて、次に何を作ろうか悩んでいた時にこのような記事を見つけました。
今回はこの記事を参考にし、電卓アプリケーションを作ることによりReactとReduxの理解を深めていきたいと思います!
前回の記事
#準備
##何はともあれcreate-react-appから
terminalで以下のコマンドを入力しましょう!
$ create-react-app calculator
$ cd calculator
$ yarn start
これで、以下のようにReactを用いたWebアプリケーションを作成する準備ができました!
create-react-app
がわからない人はこちらの記事へ
##Reduxのインストール
さらに今回はReduxを用いるので、以下のコマンドを入力してReduxのパッケージをインストールしましょう!
yarn add react-redux redux
これで準備完了です!
#早速実装してみよう!
##見た目から入る
まずは電卓らしい見た目を作っていきましょう!以下の3つのComponentを作成します
『0~9の数字ボタン(NumBtn.js)』
import React from 'react';
const NumBtn = ({n}) => <button>{n}</button>;
export default NumBtn;
『+, -, ×, ÷の計算ボタン(OperatorBtn.js)』
import React from 'react';
const OperatorBtn = ({o}) => <button>{o}</button>;
export default OperatorBtn;
『計算結果を表示する(Result.js)』
import React from 'react';
const Result = ({result}) => <div className="resultValue">{result}</div>;
export default Result;
この3つのComponentをApp.js
にimportしましょう!
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
にてスタイルを変更すると
#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;
}
App.css
を変更して、ご自身の好きなデザインにするのも面白いかもしれませんね!
これにて見た目は完成です!
#中身を実装しよう
##Action
Componentを押された時に発行されるActionの定義を行なっていきましょう!ここでは
- 『数字』が押された時
- 『+、-、×、÷』が押された時
- 『=』が押された時
- 『AC』が押された時
のActionの定義を行なっていきたいと思います。
まずは、Actionのtypeの定義から
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クリエイターを作成します。
import * as actionTypes from '../utils/actionTypes';
export const onNumClick = number => ({
type: actionTypes.INPUT_NUMBER,
number,
});
export const onPlusClick = () => ({
type: actionTypes.PLUS,
});
export const onMinusClick = () => ({
type: actionTypes.MINUS,
});
export const onMultiplyClick = () => ({
type: actionTypes.MULTIPLY,
});
export const onDivideClick = () => ({
type: actionTypes.DIVIDE,
});
export const onEqualClick = () => ({
type: actionTypes.EQUAL,
});
export const onClearClick = () => ({
type: 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』となってしまいます。
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を結合させます。
import {combineReducers} from 'redux';
import calculator from './calculator';
const reducer = combineReducers({
calculator,
});
export default reducer;
##Store
次は先ほど作成したReducerをもとに状態を保管するStoreを作りましょう。これによって、Reducerによって新しく作成された状態(Store)をAppコンポーネントに渡すことができます。
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.js
にmapStateToProps
とmapDispatchToProps
を追加し、connect
で結合させましょう!
mapStateToProps
Storeが持つ状態(state)をどのようにpropsに混ぜ込むかを決める
mapDispatchToProps
Reducer にアクションを通知する関数dispatchをどのようにpropsに混ぜ込むかを決める
bindActionCreators()
Action を追加するたびにconnectに追加するのは大変なのでbindActionCreators を使って自動的にマッピングする
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.onDivideClick()} />
<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も変更します。
import React from 'react';
const NumBtn = ({n, onClick}) => <button onClick={onClick}>{n}</button>;
export default NumBtn;
import React from 'react';
const OperatorBtn = ({o, onClick}) => <button onClick={onClick}>{o}</button>;
export default OperatorBtn;
以上で完成です。
電卓としての機能はまだ不十分なところもございますので、この記事を参考にしつつ新しい機能をつけていただけたら幸いです!
#リファレンス