Help us understand the problem. What is going on with this article?

電卓アプリで学ぶReact/Redux入門(実装編)

はじめに

React.jsとReduxを理解するために簡単な電卓アプリを作成しました。
今回はWelcome to Reactから始まり、実際にReact/Reduxで電卓アプリを作るまでの手順を説明したいと思います。
特にReduxのデータフローについては言葉や図だけではよくわからないと思うので、電卓アプリを実際に作成することで少しでも理解の助けになればいいなと思っております。
今回の記事では実装がメインとなっているため、React.jsやReduxの概念などについては省略しております。基礎的な部分を理解したい方は「Reduxの電卓アプリで学ぶReact/Redux入門(基礎知識編)」を読んでからこちらの記事を読むと理解がしやすいかと思います。

電卓アプリはcreate-react-appをベースに作成し、簡単のために加算の機能のみ作ります。

create-react-appについて

まず電卓アプリのベースを作成するためのcreate-react-appについて説明します。
create-react-appとは簡単にReact.jsのアプリケーションの雛形が作成できるコマンドラインツールです。
React.jsでアプリケーションを作りたい場合は本来であれば、JSX(JavaScriptの中にHTMLタグのような形でReactのComponentを書く形式のもの)形式で保存し、それをWebpackやBabelを利用してコンパイルし、JavaScriptのコードに変換するという作業が必要になります。
しかし、create-react-appを利用することでそういったビルドツールの準備をすることなくReact.jsのアプリケーションを作成することができます。

作成する電卓アプリについて

今回作成する電卓アプリの完成形は以下のような機能を持っています。

  • 加算のみを実装
  • 数字ボタンを押すとResultに入力した数字が出る
  • 数字ボタンを連続して押すと一つの数字として扱う(「2」「5」と押したら「25」と認識される)
  • プラスボタンを押すとResultに今まで入力された値の加算結果が表示される

準備

まずは電卓アプリを作るための準備を行います。

create-react-appのインストール

まずはcreate-react-appをインストールします。なお、Node.js(version 4以上)のものがインストールされている必要があります。
yarnの場合はyarn global add create-react-app --prefix /usr/localでcreate-react-appをグローバルインストールできます。

npm install -g create-react-app

雛形の作成

以下のコマンドで、新しくredux-calculatorのディレクトリができ、そのディレクトリにReact.jsでアプリを作成するための雛形が作成されます。

create-react-app redux-calculator

アプリの起動確認

create-react-appで作成されたアプリケーションが正しく動くか確認します。
http://localhost:3000 にアクセスしてアプリが起動できていることを確認してください。

cd redux-calculator
npm start

Reduxのインストール

今回、Reduxを利用するため、パッケージを追加します。

npm install react-redux redux

電卓アプリの全体像について

実装に入る前に全体像について説明をします。

ディレクトリ構成

電卓アプリの最終的な構成は以下のようになっています。

cd redux-calculator/src
tree -L 2

├── index.js
├── containers
│   └── CalculatorContainer.js
├── components
│   ├── NumBtn.js
│   ├── PlusBtn.js
│   └── Result.js
├── actions
│   └── index.js
├── utils
│   └── actionTypes.js
└── reducers
    ├── calculator.js
    └── index.js

  • containers

    • src/index.jsから呼ばれるContainerを格納するフォルダ
    • Containerはアプリの一番外側に存在するルート的な役割を担い、ここから各Componentが呼ばれる
  • components

    • React.jsのComponentを格納するフォルダ
    • React/ReduxにおいてはContainerから呼び出される
  • actions

    • 電卓アプリで必要となるAction(データの状態を変えるためのリクエスト)を格納するディレクトリ
    • Actionはただのオブジェクトであり、ロジックはReducerに書かれている
  • utils

    • Actionの種類を列挙しているactionTypes.jsを格納するディレクトリ
  • reducers

    • Actionのロジック部分であるReducerを格納するフォルダ
    • Actionに基づいて、新しい状態を返す

実装

ここから実際に電卓アプリを作るための実装を行なっていきます。

Containerの作成

index.jsからContainerを呼ぶように変更します。

src/index.js
 import React from 'react';
 import ReactDOM from 'react-dom';
-import App from './App';
+import CalculatorContainer from './containers/CalculatorContainer';
 import './index.css';

 ReactDOM.render(
-  <App />,
+  <CalculatorContainer />,
   document.getElementById('root')
 );

次に、Containerに電卓のモックを作成します。

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;

この状態でnpm startを実行するとブラウザ上で電卓の形が表示されると思います。

Componentの作成

Containerの中身をComponentに分解します。
まずは先ほど作成した<button>タグをComponent(NumBtn, PlusBtn, Result)に置き換えます。
Componentから呼び出されるActionについてはここでは一旦考えず、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>
           <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 />

次に各Componentを作成します。
これで先ほどContainerを複数の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;

Actionの作成

Componentで発行されるActionの定義をしていきます。
電卓アプリでいう「数字ボタンを押したとき」と「プラスボタンを押したとき」という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,
});

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

Reducerの実装

先ほど作成したActionのロジックにあたるReducerを作成していきます。
電卓アプリにおいて管理しておくべきデータの状態には「押されたボタンの値(inputValue)」「合計値(resultValue)」「計算結果を表示するかどうか(showingResult)」の3つがあります。
ここでは、各状態が各アクションによってどのように変更されるのかを記述します。

initialAppStateは初期状態です。

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;

今回はReducerはcalculator.jsというものしかありませんが、Reducerは複数組み合わせて使うことができます。
src/reducers/index.jsは複数Reducerが存在したときにそれらを組み合わせるためのものです。

src/reducers/index.js
import { combineReducers } from 'redux';
import calculator from './calculator';

const reducer = combineReducers({
  calculator,
});

export default reducer;

Storeの作成

データの状態を表すStoreを作成します。
作成するためには、以下のようにsrc/index.jsを変更します。
ここではReducerによって変更された状態(Store)をContainerに渡すという動作が行われています。
これにより、Reduxで作成されたデータの状態がReact.jsに渡るようになります。

src/index.js
import React from 'react';
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')
);

Containerの作成(React.jsとReduxの結合)

ここまででContainer, Component, Action, Reducerといった各パーツが作成できました。
しかし、各ComponentでどういったActionが発行されるかという設定をまだ行なっていませんでした。
ここでは、先ほどまでで実装したComponentとActionをContainerの中で連携させます。
つまり、ComponentがonClick()されたときのActionを定義します。

さらに、CalculatorContainer.jsの下のほうに新しくmapStatemapDispatch(dispatch)を追加しています。
mapStateはComponentのプロパティとデータの状態をバインドするものです。
mapDispatchbindActionCreatorsというActionを登録するたびに動的に追加してくれるメソッドを呼び出すものです。

そして、CalculatorContainerとしてexportしていたものをconnect(mapState, mapDispatch)(CalculatorContainer)と変更しています。
これはreact-reduxconnectメソッドを利用し、connect(mapState, mapDispatch)(CalculatorContainer)とすることで、ContainerからActionが発行されてState(データの状態)を変更できるようにするためです。
これにより、React.jsのComponentと、Reduxで管理しているデータの状態が結合できるようになるわけです。

resultのcalculator.showingResult ? calculator.resultValue : calculator.inputValueの部分はshowingResultの状態によって入力値を表示するか加算結果を表示するかを場合分けする判定ロジックです。

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>
        <Result result={calculator.showingResult ? calculator.resultValue : calculator.inputValue} />
      </div>
    );
  }
}

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

function mapDispatch(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

export default connect(mapState, mapDispatch)(CalculatorContainer);

Componentも併せて変更します。

src/components/NumBtn.js
import React from 'react';
import PropTypes from 'prop-types';

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

NumBtn.propTypes = {
  onClick: 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;

完成

以上で電卓アプリの作成は完了になります。npm startで電卓アプリがちゃんと動くことを確認してみてください。

終わりに

今回は電卓アプリを例にしてReact/Reduxの説明をしました。
今回のソースコードはGitHubのこちらのリポジトリにあげてあるので興味のある方はご覧になってください。公開しているソースコードには加算以外にも減算とリセットの機能も追加してあります。
なお、自分はまだReact/Reduxについて勉強し始めたばかりの者です。もし説明の中で間違った表現をしていたりソースコードで変えたほうがいい部分があったりした場合はフィードバックをいただければと思っております。

by @nishina555

nishina555
Webデベロッパーです。現在は業務委託で仕事をしています。サーバーサイドがメイン。Rails/React/Redux/Node/GraphQL/AWS。大学院時代は自然言語処理の研究を行っていました。
https://nishinatoshiharu.com/
onecareer
ワンランク上のキャリアを目指す学生のための新卒採用サービスONE CAREERの開発・運営会社
https://www.onecareer.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした