最小のReact+Redux

  • 42
    Like
  • 2
    Comment
More than 1 year has passed since last update.

多分これが一番小さいと思います。

前置き

この記事はReactチュートリアルを終え、そのままReduxに入門した初心者が書いています。
もし間違っていることがあれば教えていただけると幸いです。

普通にチュートリアル見て分かる人は見る必要ないと思います。

試した環境

  • OS:CentOS release 6.7 (Final)
  • Node.jsバージョン:v6.1.0

Reduxわからん

Reactチュートリアルを終え、jQueryやVanillaから仮想DOMに置き換えるとなんかすごいわかりやすい!感動!となっていたところ、なにやらReduxなるものがあるということを耳にする。

データの流れを一方向にする?確かにわかりやすそうだ!早速やってみよう!と公式やQiita等で検索して写経してみたものの、Action, Reducer, container等のよくわからない単語ばっかりでさっぱりでした。

ActionがReducerでReducerはAction + State => NewStateで……

わからなさすぎたので、いろいろな場所から情報をかき集めてきて最小のReact+Redux構成を作ってみました。

ソースコード

index.jsx
import React, { Component } from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";


// React Component
class Container extends Component {
    render() {
        return (
            <div>
                <button onClick={ e => {
                    this.props.dispatch (
                        // ReducerにActionをディスパッチする
                        { type: "INCREMENT" }
                    );
                    console.log("pushed button");
                }}>
                    button
                </button>
                <div>
                    { this.props.value }
                </div>
            </div>
        );
    }
};
// NewComponent = connect(Componentからdispatchされたアクション) (Component)
const App = connect(
    state => state
)(Container);


// Reducer
const reducer = (state = { value: 0 }, action) => {
    // Componentの中でディスパッチされたActionがaction変数に入ってくる
    // action = { type: "INCREMENT" }
    switch (action.type) {
        case "INCREMENT":
            // valueに+1して返す
            return Object.assign({}, { value: state.value + 1 });
        default:
            return state;
    }
}


// Reducerの戻り値を新しい状態(State)としてStoreで管理する
const store = createStore(reducer);


// Root Render
// ReduxのProviderコンポーネントで、Appコンポーネント
// (connect関数にComponentを渡して作成したNewComponent)をラップし、Storeを渡す
// (簡単に言えば)propsにStateが入る
render (
    <Provider store={ store }>
        <App />
    </Provider>,
    document.getElementById("root")
);

やってること

画面にボタンと初期値「0」が表示され、ボタンを押すたびに数字がインクリメントされます。

解説

import文

import文
import React, { Component } from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";

見ての通りですが、React・Redux・React-Reduxから必要なものをimportしています。
それぞれの使い方は後から出てくるので、そちらを見てください。

React Component

ReactComponent
class Container extends Component {
    render() {
        return (
            <div>
                <button onClick={ e => {
                    this.props.dispatch (
                        // ReducerにActionをディスパッチする
                        { type: "INCREMENT" }
                    );
                    console.log("pushed button");
                }}>
                    button
                </button>
                <div>
                    { this.props.value }
                </div>
            </div>
        );
    }
};

こちらはReact入門までやった人ならなんとなくわかるのではないでしょうか?
ES2015シンタックスであるclass構文を使っていますが、React.createClassと大体同じです。
同じくArrow functionsも使用していますが、こちらについては各自ググってください。(丸投げスタイル)
実行すると(大体)下記のようなDOMが生成されます。

実行結果
<div>
    <button>button</button>
    <div>0</div>
</div>

JSXに書いてあった内容ほぼそのままですね。
(this.props.value == 0として書いてます。)

ただし、1箇所Reactではない(Redux成分を含む)箇所があります。

Redux成分
<button onClick={ e => {
    this.props.dispatch (
        // ReducerにActionをディスパッチする
        { type: "INCREMENT" }
    );
}}>

この部分です。

コード内のコメントの通り、Actionのディスパッチを行なっています。
ボタンがクリックされたら(onClick)、dispatch関数を呼ぶようになっています。
dispatch関数の引数にAction(=JavaScriptのオブジェクト)を渡すと、後述のReducerにActionが渡されます(ディスパッチされます)。

Actionにはtypeを含める必要があります。
このtypeはReducerで処理を分けるためにあるので、かぶらなければなんでもいいです。

connect

connect関数は、React-Reduxの関数です。ReactとReduxを繋いでいます。

connect関数
// NewComponent = connect(Componentからdispatchされたアクション) (Component)
const App = connect(
    state => state
)(Container);

Provider(後述)に渡されたStore(後述)を、自分が定義したReact Component(今回はContainerクラス)のthis.propsに入れています。
説明は最後のrenderでしたほうがわかりやすいので、ここではあまり説明しません。

Reducer

Reducer
// Reducer
const reducer = (state = { value: 0 }, action) => {
    // Componentの中でディスパッチされたActionがaction変数に入ってくる
    // action = { type: "INCREMENT" }
    switch (action.type) {
        case "INCREMENT":
            // valueに+1して返す
            return Object.assign({}, { value: state.value + 1 });
        default:
            return state;
    }
}

この関数は、2つの引数を受け取ります。

  • state …… 現在のState
  • action …… React ComponentからディスパッチされたAction

Reducerでは、「必要なデータ」と「現在のState」を使って「やりたいこと」を実行する処理です。
action内のtypeによって処理を分けます。

Actionというのは、「やりたいこと(type)」と「必要なデータ」をひとまとまりにしたものです。
「やりたいこと」をswitch文で判断して、適切な処理を実行します。
「やりたいこと」が終わると、新しいStateが出来上がります。

これが「Action + State => NewState」の正体です。

(2016/06/06 修正・追記)

コメントで教えていただいたのですが、現在のstateから新しいstateを作成するので、(Redux的に)正しくは下記のようになります。

修正前
return Object.assign({}, { value: action.value + 1 });

修正後
return Object.assign({}, { value: state.value + 1 });

現在の状態はstateに持っているので、そちらの値を使用するのがいいようです。
dispatch関数でvalueを渡す必要がなくなり、より簡潔になりました。
(この修正により、サンプルコードの中で、Actionの「必要なデータ」はなくなりました。)

createStore

Store生成
// Reducerの戻り値を新しい状態(State)としてStoreで管理する
const store = createStore(reducer);

createStore関数はReduxの関数です。
上で説明したReducerによって作られたStateを、Store内で管理します。
よくわからないですが、Reduxではそういう決まりみたいです。

render()

render処理
// Root Render
render (
    <Provider store={ store }>
        <App />
    </Provider>,
    document.getElementById("root")
);

これもReactをやっている方なら大体わかるかと思います。
違うところは、Providerでconnect関数によって作られたAppをくるんでいるところですね。

このProviderはReact-Redux成分で、前述したconnect関数とセットで、ReactとReduxを繋ぐ役割があります。

データの流れは、

  1. Providerのstoreに渡された値が、中のAppに渡る
  2. Appに渡ってきたデータがconnect関数に渡る
  3. connect関数では、受け取った値をそのまま返す
  4. すると、Containerのthis.propsにデータが入る
  5. Container(React Component)内ではthis.propsから見ることができる

となります。

処理の流れ

自分の理解では、このソースの上から下に処理が流れていくイメージになります。
捉え方によっては、もしかしたら下から上になるかもしれないです。

最後に

あくまで自分の理解なので、間違っている部分や伝わりづらい部分があるかと思いますが、その点はご了承ください。
写経だけでなく、自分で作るって大事だなぁと思いました。(小並感)