React + Redux で電卓アプリを作成してみました



業務では Vue.js を使っていて React.js は使ったことがなかったのですが、勉強するために React/Redux を使ってアプリケーションを作成してみました。





*React.js とは

Facebook製のOSSで、MVCモデルのViewのみを提供するライブラリです。
JSXという記法を使って javaScript 内にHTMLタグを注入することができます。
ライブラリなので、他の Angular.js などとも併用できます。
カプセル化されたコンポーネントを構築することで複雑なUIもコンポーネント呼び出しで作成することができるようになり、規模が大きくなっても管理しやすいといったメリットがあります。
また、仮想DOMが高速なのでパフォーマンスが良くなるという特徴もあります。
  • 仮想DOM
    通常はDOMが状態を持っているのでDOMの更新時はDOMツリーを再構築する必要があるのですが、負荷がかかってしまい速度が低下するといった問題がありました。
    これを解決したのが仮想DOMで、DOMの更新時にオブジェクトの差分更新をして実際のDOMに反映することにより、負荷を削減し高速でレンダリングすることができるようになりました。
    (レンダリングはreact-domというモジュールがやってくれます)無駄なレンダリングを抑えることができるというメリットがあります。
  • render
    viewを描画することです。
    クラスコンポーネントではrender()を1つだけ定義する必要があります。
  • JSX
    {}でくくるとその中に javaScript を書くことができる記法です。
    コンポーネントは1つだけのJSX要素を必ず return する必要があります。
  • state
    コンポーネントが持つことのできる状態です。
    state が更新されると view が再renderされます。
  • props
    コンポーネントにおける引数のことです。
    変数と関数を渡すことができます。


*今回作るもの

記事を参考にさせていただき、電卓アプリを作成しました。
記事の内容と同様に最終的には下記構成になります。
redux-calculator
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
 ├── App.css
 ├── App.js
 ├── App.test.js
 ├── actions
 │   └── index.js
 ├── components
 │   ├── NumBtn.js
 │   ├── PlusBtn.js
 │   └── Result.js
 ├── containers
 │   └── CalculatorContainer.js
 ├── index.css
 ├── index.js
 ├── logo.svg
 ├── reducers
 │   ├── calculator.js
 │   └── index.js
 ├── serviceWorker.js
 └── utils
     └── actionTypes.js













*参考



*環境

  • MacOS
  • npm 6.3.0
  • Node 10.7.0
  • create-react-app 3.0.1
  • react-dom 16.8.6
  • react 16.8.6
  • react-scripts 3.0.1
  • redux 4.0.1
  • react-redux 7.1.0


*雛形の作成

create-react-appというコマンドラインツールを使ってWebアプリケーションの雛形を作成します。
create-react-app {アプリ名}のコマンドを実行すると、build の設定やデバッグ用サーバなどの準備を1コマンドで全て実施してくれます。
create-react-appをインストールし、作業ディレクトリで実行します。
$ sudo npm install -g create-react-app
$ create-react-app redux-calculator

実行後に指定したアプリ名のフォルダが作成されます。
補足ですが、デフォルトでmasterブランチにInitial commit from Create React Appという名前でコミットされているので git を自分で入れる必要がなくて便利です。

試しに作成した雛形Webアプリケーションを起動してみます。
$ cd redux-calculator/
$ npm start

下記のサンプル画面が表示されます。












*Redux のインストール

ReduxはFluxをベースにした状態管理ライブラリです。
今回これを使うのでインストールします。
$ npm install react-redux redux


*モックの作成

電卓アプリのモックとなるContainerのコンポーネントを作成します。
Containerとは react と redux をつなぐためのものです。このContainerに後で出てくるComponent(redux が提供する store データと actionを受け取って動作する最小構成の部品)を追加することになります。

containersディレクトリを作成してその中にCalculatorContainer.jsを作成します。
<src/containers/CalculatorContainer.js>
import React, { Component } from 'react';

class CalculatorContainer extends Component {
  render () {
    return (
      <div>
        <div>
          <button>1</button>
          <button>2</button>
          <button>3</button>
        </div>
        <div>
          <button>4</button>
          <button>5</button>
          <button>6</button>
        </div>
        <div>
          <button>7</button>
          <button>8</button>
          <button>9</button>
        </div>
        <div>
          <button>0</button>
          <button>+</button>
        </div>
        <div>
          Result: <span>some value</span>
        </div>
      </div>
    );
  }
}

export default CalculatorContainer;
  • export default
    その関数がimportされるときにdefaultで呼ぶようにします。defaultをつけない場合は、呼び出し元で呼ぶ関数を個別で指定する必要があります。
    import {Comp1} from './comp/comp1';

src/index.jsで上で作成したCalculatorContainerを呼び出します。
<src/index.js>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// ↓追加
import CalculatorContainer from './containers/CalculatorContainer';
import * as serviceWorker from './serviceWorker';

// ↓追加
ReactDOM.render(<CalculatorContainer />, document.getElementById('root'));









この段階では、初期表示のレイアウトのみできました。
次にボタンを押したときの動作を追加していきます。


*ボタン部品を作成

Containerの中のボタンをComponentに分割します。
componentsディレクトリを作成し、その中にNumBtnPlusBtnResultのComponentを作成します。
<src/components/NumBtn.js>
import React from 'react';

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

export default NumBtn;

<src/components/PlusBtn.js>
import React from 'react';

const PlusBtn = () => (
  <button>+</button>
);

export default PlusBtn;

<src/components/Result.js>
import React from 'react';

const Result = () => (
  <div>
    Result: <span>some value</span>
  </div>
);

export default Result;

<button />タグで書いていた部分を、作成したComponentに修正します。
<src/containers/CalculatorContainer.js>
import React, { Component } from 'react';
// ----- ↓追加 -----
import NumBtn from '../components/NumBtn';
import PlusBtn from '../components/PlusBtn';
import Result from '../components/Result';

class CalculatorContainer extends Component {
  render () {
    return (
      <div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={1} />
          <NumBtn n={2} />
          <NumBtn n={3} />
        </div>
        <div>
          // ----- ↓修正 start -----
          <NumBtn n={4} />
          <NumBtn n={5} />
          <NumBtn n={6} />
        </div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={7} />
          <NumBtn n={8} />
          <NumBtn n={9} />
        </div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={0} />
          <PlusBtn />
        </div>
        <div>
          // ----- ↓修正 -----
          <Result />
        </div>
      </div>
    );
  }
}

export default CalculatorContainer;


*ボタン押下時のActionを作成

ボタンが押されたときのActionを作成します。
ActionとはReduxでStoreのstateを変更するためのメッセージのことです。
何か処理を起こすための起点となります。
実行されている処理の種類を示すプロパティをactionTypesとして定義します。これは通常、文字列の定数として定義します。今回は数字ボタンを押したときと、プラスボタンを押したときで処理を分ける必要があるので、それぞれ定義しておきます。

<src/utils/actionTypes.js>
export const INPUT_NUMBER = 'INPUT_NUMBER';
export const PLUS = 'PLUS';

actionsディレクトリを作成し、index.jsを作成して数字ボタンを押したときとプラスボタンを押したときのActionの定義をします。
<src/actions/index.js>
import * as actionTypes from '../utils/actionTypes';

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


*ボタン押下時のReducerを作成

Actionで発動した動作のロジックにあたる部分をReducerと呼びます。
Action → Reducer → stateに反映 という処理の流れになります。
reducersディレクトリを作成し、calculator.jsを作成します。
<src/reducers/calculator.js>
import * as actionTypes from '../utils/actionTypes';

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

const calculator = (state = initialAppState, action) => {
  if (action.type === actionTypes.INPUT_NUMBER) {
    return {
      ...state,
      inputValue: state.inputValue * 10 + action.number,
      showingResult: false,
    };
  } else if (action.type === actionTypes.PLUS) {
    return {
      ...state,
      inputValue: 0,
      resultValue: state.resultValue + state.inputValue,
      showingResult: true,
    };
  } else {
    return state;
  }
};

export default calculator;
calculatorはアロー関数式になっていて、stateactionを引数として受け取ります。stateにはinitialAppStateのオブジェクトの値が格納され、ボタンが押されるたびにinitialAppStateの状態を更新します。
数字ボタンが押されたときの処理でstate.inputValue * 10になっているのは、数字ボタンを連続で押したときに、最初に入力した数字の位が上がるようにしているためです。

  • Spread operator(スプレッドオペレータ)
    「…」は ES2015(ES6)からの新しい演算子でスプレッドオペレータと呼びます。0以上の引数、またはArrayリテラルでは0個以上の要素としてiterableオブジェクトを展開します。
    (参考:スプレッド構文:MDN docs

  • アロー関数式
    ES2015(ES6)から利用可能になった新しいJavaScriptの構文の一つです。functionを書かなくて済むためより短く記述することができるようになっています。
    (例:let a = (y) => console.log(y);

同じディレクトリにindex.jsを作成し、combineReducers()を使って作成したReducerをまとめます。今回作成したReducerはcalculatorのみですが、複数作成することができます。
<src/reducers/index.js>
import { combineReducers } from 'redux';
import calculator from './calculator';

const reducer = combineReducers({
  calculator,
});

export default reducer;


*データの状態管理を作成

データの状態を管理できるようStoreを作成します。
srcディレクトリ直下のindex.jsを修正し、Reducerで変更された状態からcreateStore()でStoreを作成し、Containerに渡すようにします。
<src/index.js>
import './index.css';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import CalculatorContainer from './containers/CalculatorContainer';
import reducer from './reducers';

const store = createStore(reducer);

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


*部品とReduxを結合

状態を管理するためのAction、Reducer、Storeと、最初に作成したContainer、Componentsを結合していきます。

CalculatorContainer.jsにデータをバインドする処理と、ボタンを押したときのActionの定義を追加します。
<src/containers/CalculatorContainer.js>
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

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

class CalculatorContainer extends Component {
  render () {
    const { calculator, actions } = this.props;
    return (
      <div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={1} onClick={() => actions.onNumClick(1)} />
          <NumBtn n={2} onClick={() => actions.onNumClick(2)} />
          <NumBtn n={3} onClick={() => actions.onNumClick(3)} />
        </div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={4} onClick={() => actions.onNumClick(4)} />
          <NumBtn n={5} onClick={() => actions.onNumClick(5)} />
          <NumBtn n={6} onClick={() => actions.onNumClick(6)} />
        </div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={7} onClick={() => actions.onNumClick(7)} />
          <NumBtn n={8} onClick={() => actions.onNumClick(8)} />
          <NumBtn n={9} onClick={() => actions.onNumClick(9)} />
        </div>
        <div>
          // ----- ↓修正 -----
          <NumBtn n={0} onClick={() => actions.onNumClick(0)} />
          <PlusBtn onClick={actions.onPlusClick} />
        </div>
        <div>
          // ----- ↓修正 -----
          <Result result={calculator.showingResult ? calculator.resultValue : calculator.inputValue} />
        </div>
      </div>
    );
  }
}

// ----- ↓追加 -----
const mapState = (state, ownProps) => ({
  calculator: state.calculator,
});

// ----- ↓追加 -----
function mapDispatch(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

// ----- ↓追加 -----
export default connect(mapState, mapDispatch)(CalculatorContainer);

Componentsもボタンが押されたときにActionが動作するよう修正します。
<src/components/NumBtn.js>
import React from 'react';
import PropTypes from 'prop-types';

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

NumBtn.PropsTypes = {
  conClick: PropTypes.func.isRequired,
};

export default NumBtn;

<src/components/PlusBtn.js>
import React from 'react';
import PropTypes from 'prop-types'

const PlusBtn = ({ onClick }) => (
  <button onClick={ onClick }>+</button>
);

PlusBtn.propTypes = {
  onClick: PropTypes.func.isRequired,
};

export default PlusBtn;

<src/components/Result.js>
import React from 'react';

const Result = ({ result }) => (
  <div>
    Result: <span>{ result }</span>
  </div>
);

export default Result;
  • prop-types
    Reactでコンポーネントを定義する時に、PropTypesを指定することでpropsにおける引数の入力チェックを行うことができます。 数値や文字列、配列などのバリデーションを行いたい時に便利です。


*アプリケーションの起動

下記コマンドを実行すると、電卓アプリケーションが起動します。
$ npm start









*所感

ActionやReducerといった新しい概念がたくさん出てきたので、理解するのに時間がかかりました。コンポーネント単位で書けたり宣言的に処理を書ける部分は良いのですが、javaScriptの仕様を知っていないと書けない部分が多かったり設計指針の理解に時間がかかるので、Vue.js より難易度は高かったです。
データの更新の差分は高速で反映されるのですが、その反面アプリケーション起動時のビルドは遅いと感じました。
今回はCSSを適用したりしていないので、次回はそういった細かいところも学習を進めていこうと思います。


Previous
Next Post »

人気の投稿