Redux勉強の記録

更新日時: April 28, 2020

Redux

  • state管理のライブラリ

  • 複雑なアプリケーションをReact単体より簡単に作れる

Reduxサイクル

Redux 保険会社に例える
Action Creator 保険の申請を出す
Action フォーム
dispatch 受付人
Reducer 担当部署
State 保存された部署のデータ
  • 保険会社はお客様からの保険の申請から始まる

    • ReduxAction Creatorからすべてが始まる

    • JavaScriptのオブジェクトを作るか返す役割を持つ関数

  • 保険の申請には保険会社にどんなデータを持ってどう変えてほしいのかが書かれている

    • データをどのように更新したいのか、そしてそのデータは何なのかがActionに記載されている

    • typeはデータをどう変えたいかについて意図を表す

      • アクションの種類を一意に識別できる文字列またはシンボル
    • payloadは加えたい変更に関するコンテキスト

      • Actionの実行に必要な任意のデータ
  • 保険会社の受付人が申請のフォームをコピーして担当の部署に送る

    • dispatchはActionの中のオブジェクトをコピーしてReducerに送る
  • 担当部署では、それぞれの部署に合った保険会社のデータを扱う

    • ReducerはActionから送られたtypepayloadをもとにデータを更新し、そのデータ群が集まるところに返す
  • 部署で扱ったデータは中央集約管理される場所に集まって管理される

    • StateはReducerが作り出したデータをReduxで管理しやすいように中央集約しておいたデータ群

    • アプリケーション内で扱うデータをすべて管理するため、Reduxが使える形にしたデータの集まり

    • これにより、各々のReducerで現在のstateを確認する必要がなくなった

Action CreatorとAction

// People dropping off a form (Action Creator)
const createPolicy = (name, amount) => {
  // Action Creatorは生のJavaScriptオブジェクトを返す
  // 生のオブジェクトにtypeとpayloadという中身を入れてActionという参照を使う
  return { // Action (a form in our analogy)
    type: 'CREATE_POLICY',
    payload: {
      name: name,
      amount: amount
    }
  };
};

dispatch

dispatchはRedux自体に含まれているメソッド

Reducers

  • Action Creatorで作られたActionからReducerが呼ばれる

  • 何があってもReducer内で既存のStateを新しい状態に更新しない

    • Reducer内から返すのはいつも新しいオブジェクトにする

    • Reducerの役割に沿ったActiontypeがあったら、既存のStateActionpayloadを加えた新しいオブジェクトを返す
      return [...oldListOfClaims, action.payload];
      

      スプレッド構文 - JavaScript | MDN

      JSのスプレッド構文を理解する - Qiita

    • pushを使って既存のStateActionpayloadを追加して返すような書き方は厳禁

    • 既存のStateから特定のpayloadを取り除く場合も新しいオブジェクトとして 生成して返す
      return oldListOfClaims.filter(claimName => claimName !== action.payload.claimName);
      

      Array.prototype.filter() - JavaScript | MDN

  • combineReducersで指定するキーはstateで扱うための名称になる

Store

  • Reduxを使ってStateを操作するときに、createStorecombineReducersを使う

  • combineReducersでは複数のファイルでそれぞれ指定したReducerの設定を1つにするためのメソッド

    • 複数のReducerにおいて指定されたAction.typeAction Creatorから生成されたオブジェクト内のtypeを比較して一致する条件だけに処理が走るようになる
  • createStorecombineReducersで1つに束ねたReducerをもとに1つのオブジェクトとしてState管理を管理する

State

  • Storeされたstateに直接アクセスしてデータを変更することはできない

  • Action Creatorから生成されたオブジェクト(Action)を通してはじめてデータの変更ができる

Redux + JavaScript

A Pen by vue+typescript勉強中カモ

React-Redux

構造

- src
|- actions    // Action Creatorに関するファイル
|- components // Component
|- reducers   // Reducer
index.js      // ReactとReduxの両方を準備する。Componentで開始時に表示するファイルを指定する

初期化時の呼び出し順

index.js
reducers
↓
Provider
↓
App
↓
Component
connect
↓
mapStateToProps(Storeのstateを初期化時に適用する)
↓
mapDispatchToProps
↓
App
Component
↓
render

Reduxの処理の流れ - Qiita

ファイル内での定義

  • index.js

    • Storeを生成して使うため、reduxからcreateStoreを使う

    • StorestatereactのDOMの中でも使えるようにProviderを使う

    • stateをどのように変化させるかの定義をしたreducerを初期化するため、インポートする

      import React from 'react'
      import ReactDOM from 'react-dom'
      
      import { createStore } from "redux";
      import { Provider } from "react-redux";
      
      import reducer from "./reducers";
      
      import App from './components/App'
      
      // Storeを生成し、Reducerと紐づくことでstateの更新ができるようになる
      // Storeのstateはstore.dispatch({action:content})といった形で更新できる
      const store = createStore(reducer);
      
      ReactDOM.render(
        <Provider store={store}>// Reactのルートからstoreを管理する
          <App />
        </Provider>,
        document.getElementById("root")
      );
      
  • reducers/index.js

    • 複数のreducerの定義をまとめるcombineReducersreduxからインポートする

      • combineReducersでは、state内で扱うためのキー名とそのキーの値となるオブジェクトを返すreducerを指定する
        export default combineReducers({
        キー1: reducer1
        キー2: reducer2
        });
        
    • ルートComponentでStore生成時にReducerを初期化する

  • actions/index.js

    • stateに変化を与えるための関数Action Creator

      • 名前付きエクスポート(named export)で関数名を決める
    • 変化させるタイプやその値の規格を定義したオブジェクトAction

      • typepayloadのセットにする
  • components/App.js

    • 制御するComponentをインポートする
  • reduxstateを使うComponent内

    • 基本的にexport default connect(mapStateToProps,{アクション名})(コンポーネント名)という形式で扱う

    • stateを受け取る場合
      // 以下の形式でStateからComponentのpropsへ代える
      const mapStateToProps = (state) => {
        return { Component内で扱うpropsのキー: state.State内で扱っていたキー }
      }
      
    • ActionをComponent内で扱う場合
      import アクション名 from 'アクションが定義されたファイルのパス'
      
      export default connect(mapStateToProps,{アクション名})(コンポーネント名)
      
    • この書き方で、action関数はStoreに向けての処理をdispatchすることができるようになる

    • state管理されているかどうかはrenderconsole.log(this.props)

※明確に言語化できていない部分

  • Component内でAction Creatorがトリガーされた時、どうやってReducerが反応する/できるのか

  • おそらく、Action Creatorがトリガーされ、ActionをリターンするとcombineReducersに登録されたキー:reducerの順で処理が走るだろうと予想する

connect()()の呼び出しの謎

function connect(a=1,b=2) {
  console.log(a);
  this.arg1 = a;
  this.arg2 = b;

  //connect()で無名関数をリターンする
  return function(component='a') {
    console.log(arg1,arg2,component);
  }
}

//connect()()でリターンされた無名関数を実行する
connect(3,4)('b');

connect()()の動きをconsoleで見る

react-reduxのconnect()を図解する - Javaエンジニア、React+Redux+Firebaseでアプリを作る

  • どうやらCurryingというES6の書き方があるらしい

Redux-Thunk

役割

  • Action CreatorからAPIのリクエストを送り、レスポンスを受け取る
  • レスポンスの結果をActionpayloadに格納する

書き方

  • index.js

    • Storeを生成して使うため、reduxからcreateStoreを使う

    • StorestatereactのDOMの中でも使えるようにProviderを使う

    • stateをどのように変化させるかの定義をしたreducerを初期化するため、インポートする

    • reducerActionを送る前に非同期処理をするミドルウェアをミドルウェアを追加しておくためにapplyMiddlewareを使う

    • Action CreatorからActionを関数として返しても遅延して実行するよう、redux-thunkを使う

      import React from 'react'
      import ReactDOM from 'react-dom'
      
      import { createStore, applyMiddleware } from "redux";
      import { Provider } from "react-redux";
      import thunk from 'redux-thunk';
      
      import reducer from "./reducers";
      
      import App from './components/App'
      
      // Storeを生成し、Reducerと紐づくことでstateの更新ができるようになる
      // Storeのstateはstore.dispatch({action:content})といった形で更新できる
      // Storeを生成する際に、thunkミドルウェアを使うようにする
      const store = createStore(reducers, applyMiddleware(thunk));
      
      ReactDOM.render(
        <Provider store={store}>// Reactのルートからstoreを管理する
          <App />
        </Provider>,
        document.getElementById("root")
      );
      
  • Action Creatorを記載したファイル

    • Action Creatorの処理を手動的にdispatchすることで非同期処理が使える

    • redux-thunkcreateThunkMiddlewaredispatchする

      • Action Creatorの返り値が生のオブジェクトなら通常通り、reducerに処理を渡す

      • Action Creatorの返り値が関数ならその関数を実行する

        • 関数実行後、非同期処理が走り、dispatchで生のオブジェクトを返すようにすると通常通り、reducerに処理を渡す
          export const asyncAction = () => {
            return async (dispatch) => {
              // 非同期処理をしてactionのpayloadを生成
              const response = await someAsyncFunc();
          
              // 手動でdispatchし、redux-thunkへactionを送る
              dispatch({
                type: "ASYNC_ACTION",
                payload: response
              });
            }
          }
          

    redux-thunk/index.js at master · reduxjs/redux-thunk

    Thunks in Redux: The Basics - Fullstack Academy - Medium

    도대체 무슨 ‘척’?

    redux-thunk

    Thunk - Wikipedia

    리덕스(Redux)란 무엇인가? | 텅 빈 충만의 블로그

    리덕스 미들웨어, 그리고 비동기 작업 (외부데이터 연동) | VELOPERT.LOG

Reduxによるデータロード順

Componentが画面にレンダーされる
↓
Componentの`componentDidMount`メソッドが呼ばれる
↓
`componentDidMount`から`Action Creator`を呼ぶことで必要なデータをフェッチする
↓
`Action Creator`がAPIリクエストを送る
↓
レスポンスを受け取り、`Action``payload`に格納する
↓
`reducer``Action``type`を見て`state`を返す
↓
Componentの`mapStateToProps`を介して`render`関数で扱える状態になる

注意点

  • Action Creator内で非同期処理のレスポンスを直接受け取って使うと以下のようなエラーが発生する
    Error: Actions must be plain objects. Use custom middleware for async actions.
    
  • エラー発生の原因:async/awaitを使うことで想定してイなかったオブジェクトが返って来ている

    • await部分で呼び出した関数が生のJavaScriptのオブジェクトを返すと思うかも知れないが、実はリクエストオブジェクトを返している

    • 試しにasync/awaitの処理をbabeljsTry it outでコピペして見ると想定してなかったオブジェクトがreturnされていることがわかる

      • 注意点として、PRESETSの設定項目はすべてONにした状態で試す
    • なので、async/awaitを使っても生のJavaScriptオブジェクトは帰って来ず、Actionで返すべきオブジェクトのタイプが合わない

  • 非同期処理を使わずに同期的にデータをレスポンスしようとすると、Action Creatorがリクエスト投げている間、Actionは先にdispatchReducerまでたどり着いてしまう

    • そうすると、Componentで表示するデータがないまま、画面が表示される
  • そこで、redux内でもリクエスト/レスポンスの受送信ができるように手伝うミドルウェアが必要になり、redux-thunkがその役割を担う

ReduxにおいてのMiddleware

  • dispatchするすべてのActionで呼び出される

    • Action > dispatch > Middleware > Reducers
  • Actionを止めたり、変えたりする方法で操作が可能になる

  • 非同期的なActionを使うのが一般的な方法

Action Creatorのルールの違い

  • Redux自体の場合

    • Action Creatorは必ずActionオブジェクトを返す

    • Actionは必ずtypeを持つ

      • 場合によってはpayloadもつける
  • redux-thunkの場合

    • Action CreatorActionオブジェクトもしくは関数を返す

      • 関数を返す場合、関数の呼び出しもやってくれる
    • Actionオブジェクトが返ってきたら必ずtypeを持つべき

      • 場合によってはpayloadもつけられる

redux-thunkの詳細な動き

<Action Creator>
↓
オブジェクトもしくは関数の「何か」
↓
※`dispatch`
↓
<redux-thunk内>
↓
「何か」
↓
関数かどうか? → <関数の場合> → `getState``dispatch`を使って関数の呼び出しをする → リクエストが終わるまで待つ → リクエストが終わったら、手動で`Action`を※`dispatch`する
↓
<関数ではない場合>
↓
Reducers

reducerのルール

返り値

  • 「undefined」以外の値を返す必要がある

stateの取り扱い

  • state」、または以前のstateとアクションのみを使用してアプリ内で使用されるデータを生成する

    • 初期化時のreducerstateが何も決まっていない状態であるが、reducerに渡すときの引数をstate = nullのようにデフォルトバリューでundefined以外に指定することでエラー発生せず、初期化されたstateが生成できる

    • 初期以降は生成されたstateactionをもとにreducerを通して新たなstateを生成する

Reducer内で扱える値について

  • どの値を返すかを決定するために、「Reducerが受け付けた値」以外で返してはなりません。

    • stateactionで受け付けたもの以外にAPIリクエストを投げたり、ファイルの読み込みをしたり、DOMを直接介したりして返す値を決めてはいけない

    • stateactionのみで新しいstateを返すべき

ミスリーディングに注意する文脈

  • 入力の「state」引数を変更してはいけない

    • 入力の「state」とは、引数で渡ってきたstateのこと

    • stateの形式が配列やオブジェクトの場合、いかにも容易く変えられてしまう
      //配列
      const colors = ['red','green'];
      colors.push('purple') // ['red','green','purple']
      colors.pop() // ['red','green']
      colors[0] = 'PINK' // ['PINK','green']
      
      //オブジェクト
      const profile = { name: 'Alex' }
      profile.name = 'Sam' // { name: 'Sam' }
      profile.age = 30 // {name: "Sam", age: 30}
      
      //文字列
      const name = 'Sam'
      name[0] // 'S'
      name[0] = 'X' // name == "Sam"
      
    • 配列やオブジェクトの場合、定義した値(A)と同じ値を別で定義した値(B)とで厳格な比較をしても同一性はない。 なぜかと言うと配列やオブジェクトは参照として比較される
      //オブジェクト同士の厳格な比較
      const obj1 = {foo:1};
      const obj2 = {foo:1};
      
      obj1 === obj2 // false メモリ上のレファレンスが異なるため
      
      const obj3 = obj2;
      
      obj2 === obj3 // true メモリ上のレファレンスが同じ
      
      //配列同士の厳格な比較
      const arr1 = [1,2,3];
      const arr2 = [1,2,3];
      
      arr1 === arr2 // false メモリ上のレファレンスが異なるため
      
      const arr3 = arr2;
      
      arr2 === arr3 // true メモリ上のレファレンスが同じ
      
      //厳密に言うと、配列もオブジェクトの一種
      const arr = [1,2,3];
      const obj = { var: "foo" };
      
      typeof arr //"object"
      typeof obj //"object"
      

      Array - JavaScript | MDN

    • reduxのcombineReducerソースコードを見る限り、reducer内に引数で「渡ってきたstate」と「現在のstate」を比較してstateの更新の有無を判定している。 つまり、reducer内でstateを直接変えてしまうとstateに更新がかかるため、思わぬ動作を引き起こす可能性がある

reducerで安全にstateを更新する

  • 基本的に新しいオブジェクトや配列を返すことで安全に更新できる
//オブジェクト
//変更
{...state, 変更するキー:'変更する値'}
//追加
{...state, 追加するキー:'追加する値'}
//削除
{...state, 削除するキー: undefined}
もしくは
_.omit(state,'削除するキー') // Lodash Documentを参考

//配列
//変更
state.map(elem => elem === 変更対象の値 ? 変更後の値 : elem)
//追加
[...state, 追加する値]
//削除
state.filter(elem => elem !== 削除する値)

コメントする