プログラミング

React + Redux を1つのJavaScriptファイル内で使うサンプル

投稿日:2017年7月13日 更新日:

1. はじめに

React + Redux を説明する場合、役割ごとにファイルを分けるのが普通です。しかし、それだと複雑になって却って分かりにくかったりしますので、1つのJavaScript内に全て詰め込んだサンプルを用意しました。

2. デモページ

以下のURLでこのウェブページを見ることができます。

3. 画面

「足す」ボタンを押すと、フィールドに入力した数字をどんどん足していくだけです。足した結果は右側の「合計」のところに表示されます。
「クリア」ボタンを押すと、「合計」の値が 0になります。

デモページの画面
デモページの画面

4. JavaScript のコード(全文)

このウェブページの JavaScriptコードはすべて 1つのファイルにまとめられています。
以下がそのコードになります。

import '../../node_modules/bootstrap-sass/assets/javascripts/bootstrap.js';
import 'babel-polyfill'

import _ from 'lodash'
import React from 'react'
import { render } from 'react-dom'
import { Provider, connect } from 'react-redux'
import { createStore } from 'redux'
import PropTypes from 'prop-types'

// index.html ファイルをコピーする
require('file-loader?name=../../dist/[name].[ext]!../index.html');

//-----------------------------------
// Action creators (Actionを返す)
//-----------------------------------

const additionAction = (value) => {
  return {
    type: 'ADD_VALUE',
    value
  }
}
const clearAdditionAction = () => {
  return {
    type: 'CLEAR_VALUE',
  }
}

//-----------------------------------
// Reducer
//-----------------------------------

const additionReducer = (state = 0, action) => {
  switch (action.type) {
    case 'ADD_VALUE':
      return state + action.value
    case 'CLEAR_VALUE':
      return 0;
    default:
      return state
  }
}

//-----------------------------------
// Component
//-----------------------------------

const AdditionComponent = ({ total, onClickToAdd, onClickToClear }) => {
  let textInput;
  return (
    <div>
        <label>足し算</label>
        <input type="text"
          placeholder="10"
          defaultValue="100"
          ref={(input) => { textInput = input; }}
        />
        <button onClick={() => onClickToAdd(textInput.value) }>足す</button>
        <button onClick={onClickToClear}>クリア</button>
        <span className="result">合計:{total}</span>
    </div>
  );
};

AdditionComponent.propTypes = {
  total: PropTypes.number.isRequired,
  onClickToAdd: PropTypes.func.isRequired,
  onClickToClear: PropTypes.func.isRequired
};

//-----------------------------------
// Container
//-----------------------------------

const AdditionContainer = (() => {

  const mapStateToProps = (state/*, ownProps*/) => {
    const props = {
      total: state
    };
    return props;
  }

  const mapDispatchToProps = (dispatch) => {
    return {
      onClickToAdd: (value) => {
        const val = parseInt(value);
        if (_.isNaN(val)) {
          //
        } else {
          dispatch(additionAction(val));
        }
      },
      onClickToClear: () => {
        dispatch(clearAdditionAction());
      }
    }
  }

  return connect(
    mapStateToProps,
    mapDispatchToProps
  )(AdditionComponent);

})();

//-----------------------------------
// Store
//-----------------------------------

const store = createStore(additionReducer)

//-----------------------------------
// 画面に表示する
//-----------------------------------

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

5. 解説

React と Redux にとって重要な部分を説明します。

1. Action (Action Creator)

Action は「何が起きるか?」を表します。

今回、「値を足す」と「値をクリアする」という2つの処理がありますので、Action(とAction Creator)も2つ用意しました。

adddionAction

  • 「値を足す」を表す action creator です。
  • ADD_VALUE という文字列で表すことにします。
  • 足す値を引数として受け取って、return で返すオブジェクト(Action)に追加しています。

clearAdditionAction

  • 「値をクリアする」を表す action creator です。
  • CLEAR_VALUE という文字列で表すことにします。
  • こちらはそれ以外で必要な値はありません。
const additionAction = (value) => {
  return {
    type: 'ADD_VALUE',
    value
  }
}
const clearAdditionAction = () => {
  return {
    type: 'CLEAR_VALUE',
  }
}

※ ここでは ADD_VALUECLEAR_VALUE を文字列にしましたが、実際のアプリケーションでは定数にした方がよいでしょう(ESLintなどのツールがタイプミスを発見してくれます)。

2. Reducer

この小さなアプリケーションで State として管理する値は、「足した結果の値」だけとしました。実際は、エラーがあった場合とそうでない場合を区別するための値などもあった方がよいですが、焦点がぼやけそうなので省略しました。

以下の Reducer は、この「足した結果の値」を更新するために用意しました。この値の更新に関する処理は、この中に書きます。

今回扱う 2つの処理もこの値を更新する処理ですので、action.type で区別してここで処理しています。Reducer は「現在のstate」と「action」を引数として受け取り、「新しいstate」を返します。

「値を足す」ADD_VALUE の場合は、現在の値に、actionとして渡ってきた値を足した数を返しています。
「値をクリアする」CLEAR_VALUE の場合は、単に 0 を返しています。

const additionReducer = (state = 0, action) => {
  switch (action.type) {
    case 'ADD_VALUE':
      return state + action.value
    case 'CLEAR_VALUE':
      return 0;
    default:
      return state
  }
}

3. Component

画面となる部分です。

属性(props)として、以下の3つを受け取っています。

  • total: 足した結果の値
  • onClickToAdd: 「足す」ボタンをクリックした時のイベントハンドラ
  • onClickToClear: 「クリア」ボタンをクリックした時のイベントハンドラ

この2つのイベントハンドラは、Container 側で実装するのですが、これが Redux のやり方です。Container 内では、State を更新するための dispatch 関数を使うことができます。
「値を足す」方のイベントハンドラでは、画面上に入力された値を取得して使うことになるので、ここでは onClickToAdd にその値を渡すような形にしています。これを実現するため、onClick= のところで「入力された値を onClickToAdd に渡して実行する」という処理を無名関数を作って指定しています。少しトリッキーなやり方に思えますが、他によいやり方もないように思います。

total はそのまま画面に出力しているだけです。

最後に、propTypes で各値を定義しておきましょう。

const AdditionComponent = ({ total, onClickToAdd, onClickToClear }) => {
  let textInput;
  return (
    <div>
        <label>足し算</label>
        <input type="text"
          placeholder="10"
          defaultValue="100"
          ref={(input) => { textInput = input; }}
        />
        <button onClick={() => onClickToAdd(textInput.value) }>足す</button>
        <button onClick={onClickToClear}>クリア</button>
        <span className="result">合計:{total}</span>
    </div>
  );
};

AdditionComponent.propTypes = {
  total: PropTypes.number.isRequired,
  onClickToAdd: PropTypes.func.isRequired,
  onClickToClear: PropTypes.func.isRequired
};

4. Container

先ほど作った Component を包み込み、connect() 関数で、Redux の世界につなげます。

必要に応じて、以下の2種類の関数を作成します。

mapDispatchToProps

  • 更新された state が引数として渡ってきますので、それを component の props に渡す処理を記述します。
  • 今回はそのまま、props.total として渡しています。

mapDispatchToProps

  • component 内で使用するイベントハンドラは、ここで定義します。
  • 画面上に入力された値を使った処理を書きたい場合は、引数としてその値を受け取る形にしておくのが通常のやり方です。component 側では入力した値をここで定義したイベントハンドラに渡して実行します。
  • ここで定義したイベントハンドラは、component に props として渡されます。
const AdditionContainer = (() => {

  const mapStateToProps = (state/*, ownProps*/) => {
    const props = {
      total: state
    };
    return props;
  }

  const mapDispatchToProps = (dispatch) => {
    return {
      onClickToAdd: (value) => {
        const val = parseInt(value);
        if (_.isNaN(val)) {
          //
        } else {
          dispatch(additionAction(val));
        }
      },
      onClickToClear: () => {
        dispatch(clearAdditionAction());
      }
    }
  }

  return connect(
    mapStateToProps,
    mapDispatchToProps
  )(AdditionComponent);

})();

5. Store

Store は state を管理しているところです。state を更新する Reducer もまとめて保持しますので、createStore() 関数に Reducer を渡して Storeを作成します。

const store = createStore(additionReducer)

6. 画面に表示する

Redux が用意している Provider という component で、先ほど作成した container を包んで画面にレンダリングします。その際、Provier に store を渡します。これにより、ここまで定義してきたすべてのオブジェクトが1つにまとめられてアプリケーションとして機能します。

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

6. 動作

「足す」ときの処理は以下のように動きます。

  1. 画面上の「足す」ボタンを押すと、onClickToAdd イベントハンドラが実行されます。
  2. onClickToAdd イベントハンドラは、
    1. 画面上に入力された値を受け取り、
    2. それを additionAction という action creator に渡して action を生成します。
    3. それを、dispatch() 関数に渡して実行します。
  3. dispach() 関数は内部で各Reducerを呼び出して実行します。
  4. 今回作成した additionReducer という Reducer では、ADD_VALUE を持った action を受け取りますので、『「現在の足した値」と「画面上の値」を足した値』を返します。
  5. state は、新しい値で更新されます。
  6. mapStateToProps に更新された state (足した結果の値)が渡されます。
  7. mapStateToProps は渡された値をそのまま、component に渡します。
  8. component は新しい値を受け取り、画面を更新します。

開発する側にとってのイメージとしては、

  1. どんな処理があるかを Action (と Action Creator) で表す。
  2. Reducer で state を更新する処理を実装しておく。
  3. component と container で、以下を実装しておく。
    1. 画面を見た目を更新する処理 (React)
    2. イベントがあった時に Redux に通知する処理 (イベントハンドラでdispatchする。その際、処理の種類に合ったActionを渡す。)

をやっておけば、あとはユーザーの操作によってイベントが発生すると、

  1. component
  2. –> reducer
  3. –> stateの更新
  4. –> component に戻って画面更新

という流れで処理を行ってくれるという感じです。

「各state を更新する処理」と「各stateを元に画面を更新する処理」が分離することによって、アプリケーションが複雑になっても、実装はそれ程複雑にならない。というメリットがあります。

7. まとめ

すごく単純な処理を実装したいだけであれば、Redux + React を使うのは却って面倒なだけでしょう。どのくらいの規模から、メリットが大きくなるのかを見極めるために、今回のような最小限のサンプルが役に立つのではないかと思います。

📂-プログラミング
-

執筆者:labo


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

関連記事

JavaScript

JavaScriptで画面上の文字列をクリップボードにコピーする方法

目次1. はじめに2. Clipboard API and events を使う方法3. Selection API を使う方法(1) 基礎知識(2) プログラムの書き方(3) サンプルページ4. お …

web development

Web Development for Beginners を読む:レッスン3

目次1. はじめに2. Lesson 3: Creating Accessible WebpagesTools to useScreen readersContrast checkersLightho …

JavaScript でスロットマシーンを作ってみました(3回目)

「JavaScript でスロットマシーンを作ってみる」の3回目です。 今回は ゲームっぽくしてみました。 目次1. スクリーンショット2. デモページ3. 内容4. ソースコード5. 参考情報 1. …

Web Programming

ブラウザがウェブサーバーに送っている情報を表示するウェブページを作ってみよう(HTML/CSS/JavaScript/PHP)

目次1. はじめに2. この記事の対象とする人3. 今回の題材となるWebページとファイル3-1. 作成するページ3-2. 今回の題材となるファイルファイルのダウンロードファイルを閲覧する4. 利用す …

WordPress

WordPress のテーマ、プラグイン開発のためのデバッグ設定

WordPress のテーマ、プラグイン開発のためのデバッグ設定や Tips について、ここにまとめていこうと思います。 目次1. wp-config.php の設定WP_DEBUGWP_DEBUG_ …