2017年5月28日日曜日

React + Redux とりあえず簡単な例から

久々の投稿。ここしばらくは, モバイル, フロントがメインでたまにバックエンドの開発をする日が続いています。
仕事では, Angular 1 がメインでしたそろそろReact + webpackでいこうと模索しています。
React をしばらく触って, それからReact + Redux の学習を初めています。
React は本読んだりしてなんとかしましたが, redux にはかなり苦労しています。


参考にしたもの

一番上の解説はわかりやすかったです。今回のサンプルもここで取り上げていたサンプルをベースに作っています。 下2つはReduxの公式のサンプルです。フォルダの構成とか結構参考になると思います。

最終的なゴール

  • (今回)webpackでビルドする
  • (今回)webpack-dev-server で動作確認
  • (今回)reduxの導入
  • (今回)es6でかけるようにする
  • (今回)action, container, component, reducerの構成を作る
  • react-routerを使う
  • ajaxでデータを取ってくる
react-routerと, ajaxに関しては次回以降取り上げたいと思います。

バージョンなど

作成日 : 2017/05/28 webpack : 2.5.1
プロジェクトの作成, react, webapack, redux の導入
mkdir first     
cd first
npm init -y
npm install --save webpack webpack-dev-server
npm install --save-dev babel babel-core babel-loader babel-preset-es2015 babel-preset-react
npm install --save react react-dom react-redux redux
npm install --save prop-types
firstはプロジェクト名です。なんでも良いです。 今回は, webpack, webpack-dev-server, react, react-redux, reactのプロパティを使うとWarningが出るので prop-typesを使います。

フォルダ構成
first
|- dist
|- src
|   |- actions
|   |- components
|   |- containers
|   |- reducers
|   |- index.jsx
|- .babelrc
|- index.html
|- package.json
|- wbpack.config.js


.babelrc

ここで, es6を使うための設定をします。
{
  "presets": [
    "es2015", "react"
  ]
}


webpack.config.js

webpack の設定です。今回は jsx を動作させるための設定だけを記載しました。
const path = require('path');

module.exports = {
    entry: path.join(__dirname, 'src/index.jsx'),
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    devServer: {
        contentBase: '.',
        port: 8080,
        inline: true,
        historyApiFallback: true
    },
    module: {
        loaders: [
            {
                test: /\.jsx$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
            }
        ]
    }
};
webpackでビルドした結果を distフォルダへ, babel-loaderを使って, jsxを処理します。 webpack-dev-server はポート8080で動作させます。

Redux

Redux に関しては他のブログなどのエントリーを参考にしてさらっとまとめます。 Redux : Redux 細かい話は公式などを確認した方が良さそう。 状態管理をアプリ全体で行う, 状態管理するStoreはアプリ全体で1つ。 ユーザのActionがコールされると, Recuderを使って, Storeの状態を更新する。 ここで登場人物が3つ出てきました。(Action, Reducer, Store) Reduxではデータフローが重要で, データフローのルールに従います。 以下各要素の説明とプログラム上での登場人物の整理です。

Action

typeをキーとしてStateを更新するための定義を書く。 状態更新はdispatch関数を実行して行う (ajaxの通信はここで行う) ActionはAction名(Reducerで処理を判別するため)と状態の値を持った単なるオブジェクト Storeのdispatch() メソッドの引数にAction Creatorを渡すことでActionがReducerに送られる

ActionCreator

最終的には, Actionと同じファイルに書いている functionで Action = object を返す Actionで書いていない更新したい状態の定義を書く。 Stateの更新自体は, Reducerがするので更新したい部分を引数などから生成する

Reducer

Reducerは、現在の状態(state)と受け取ったActionを引数に取り、新しい状態を返す関数 typeをキーにして処理を分ける, Switch 文中に Action で定義した State の type で分岐しロジックを書く stateの更新には, ES2015 Object.assign()を使おう!! Object.assign() はmergeオブジェクトのマージを行います。 ※Reducerは複数作ることができます。(ページごとに作るイメージかなぁ) (storeは一つしか引数に取れないので combineReducersで combine)

Store

状態管理のオブジェクト 初期データと, ReducerからcreateStoreメソッドで作成します。これをProvider渡します。 Reduxのmiddlewareを渡す場合はそれも引数に加えます。

Container Components

親コンポーネント。ここでReactとReduxのつなぎ込みを行います。 Container ComponentsはPresentational Componentを持ちます。

Presentational Component

子コンポーネント。最小単位のコンポーネント 親からプロパティを受け取り描画します。イベントなども親から受け取ります。

サンプル

今回のサンプルは参考の一番上のエントリーを参考にreact + reduxをwebpackで動作させる, フォルダ構成をReactの公式サンプルを 元に作り変えたものです。 index.jsx エントリー部分。
import React from 'react';
import ReactDOM from 'react-dom';

import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { reducers } from './reducers/index.jsx';

import AppContainer from './containers/AppContainer.jsx';

const initialState = {
    formReducer: {
        value : null
    }
};

const store = createStore(reducers,
    initialState); // reducer, initial state

// Rendering
ReactDOM.render(
  
      
  ,
    document.querySelector('.content')
);


とりあえず必要なComponentなどを全部importしています。 ここに直接書くのは, 初期状態の設定と(この部分もファイル分離できる) store の作成と, レンダリングの部分ですね。 Reactに連携させるReduxのStoreを渡しています。Providerという部分でReactとReduxのStoreの連携をしていますね。

index.html 元になるView,ただし動作関係は全部上のindex.jsxに任せる



    
    Redux Sample


ビルドした後のjsを取り込んでいるだけです。この部分はもう基本的には変更しません。

actions/index.jsx アクションの実装です。Actionと,ActionCreatorの実装をしています。
const SEND = 'SEND';

/* Action Creator */
// Return Object
export function send(value) {
    // Action
    return {
        type: SEND,
        value,
    };
}
Actionは, objectを返します。 ここでは, Actionタイプ(アクションの名前みたいなもの)とアクションを発行した部分から valueという値を受け取ってそれを返しています。

recucers/formreducer.jsx Reducerの実装です。
export function formReducer(state = {}, action) {
    switch(action.type) {
        case 'SEND':
            return Object.assign({}, state, {
                value: action.value,
            });
        default:
            return state;
    }
}


containers/FormApp.jsx
import React from 'react';
import PropTypes from 'prop-types';

import FormInput from '../components/FormInput.jsx';
import FormDisplay from '../components/FormDisplay.jsx';

class FormApp extends React.Component {
    render() {
        return (
            
); } } FormApp.propTypes = { onClick: PropTypes.func.isRequired, value: PropTypes.string }; export { FormApp as default };


containers/AppContainer.jsx
import { connect } from 'react-redux';
import { send } from '../actions/index.jsx';
import FormApp from './FormApp.jsx';

// Connect to Redux
function mapStateToProps(state) {
    return {
        //value: state.value
        value: state.formReducer.value    //http://qiita.com/usagi-f/items/ae568fb64c2eac882d05
    }
}

function mapDispatchToProps(dispatch) {
    return {
        onClick(value) {
            dispatch(send(value));
        }
    };
}

const AppContainer = connect(
    mapStateToProps,
    mapDispatchToProps
)(FormApp)

export { AppContainer as default };
ここで Redux のプロパティとComponentを繋ぐ作業をしています。 storeからデータをひっぱてくるところ, メソッドを渡すとこですね。

components/FormDisplay.jsx
class FormDisplay extends React.Component {
    render() {
        return (
            
{this.props.data}
); } }; FormDisplay.propTypes = { data: PropTypes.string }; export { FormDisplay as default };


components/FormInput.jsx
class FormInput extends React.Component {
    send(e) {
        e.preventDefault();
        this.props.handleClick(this.myInput.value.trim());
        this.myInput.value = '';
        return;
    }
    render() {
        return(
            
(this.myInput = ref)} defaultValue="" />
); } }; FormInput.propTypes = { handleClick: PropTypes.func.isRequired }; export { FormInput as default };


動作確認

webpackでビルド, webpack-dev-serverで簡易サーバーを動かします。 ビルド
webpack
サーバーを動かす
webpack-dev-server
localhost:8080/index.html へアクセス

0 件のコメント:

コメントを投稿