Redux勉強の記録
更新日時:
Redux
-
state管理のライブラリ -
複雑なアプリケーションを
React単体より簡単に作れる
Reduxサイクル
| Redux | 保険会社に例える |
|---|---|
| Action Creator | 保険の申請を出す |
| Action | フォーム |
| dispatch | 受付人 |
| Reducer | 担当部署 |
| State | 保存された部署のデータ |
-
保険会社はお客様からの保険の申請から始まる
-
ReduxはAction Creatorからすべてが始まる -
JavaScriptのオブジェクトを作るか返す役割を持つ関数
-
-
保険の申請には保険会社にどんなデータを持ってどう変えてほしいのかが書かれている
-
データをどのように更新したいのか、そしてそのデータは何なのかが
Actionに記載されている -
typeはデータをどう変えたいかについて意図を表す- アクションの種類を一意に識別できる文字列またはシンボル
-
payloadは加えたい変更に関するコンテキストActionの実行に必要な任意のデータ
-
-
保険会社の受付人が申請のフォームをコピーして担当の部署に送る
dispatchはActionの中のオブジェクトをコピーしてReducerに送る
-
担当部署では、それぞれの部署に合った保険会社のデータを扱う
ReducerはActionから送られたtypeとpayloadをもとにデータを更新し、そのデータ群が集まるところに返す
-
部署で扱ったデータは中央集約管理される場所に集まって管理される
-
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の役割に沿ったActionのtypeがあったら、既存のStateにActionのpayloadを加えた新しいオブジェクトを返すreturn [...oldListOfClaims, action.payload];-
pushを使って既存のStateにActionのpayloadを追加して返すような書き方は厳禁 - 既存の
Stateから特定のpayloadを取り除く場合も新しいオブジェクトとして 生成して返すreturn oldListOfClaims.filter(claimName => claimName !== action.payload.claimName);
-
-
combineReducersで指定するキーはstateで扱うための名称になる
Store
-
Reduxを使ってStateを操作するときに、createStoreとcombineReducersを使う -
combineReducersでは複数のファイルでそれぞれ指定したReducerの設定を1つにするためのメソッド- 複数の
Reducerにおいて指定されたAction.typeとAction Creatorから生成されたオブジェクト内のtypeを比較して一致する条件だけに処理が走るようになる
- 複数の
-
createStoreはcombineReducersで1つに束ねたReducerをもとに1つのオブジェクトとしてState管理を管理する
State
-
Storeされたstateに直接アクセスしてデータを変更することはできない -
Action Creatorから生成されたオブジェクト(Action)を通してはじめてデータの変更ができる
Redux + JavaScript
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
ファイル内での定義
-
index.js-
Storeを生成して使うため、reduxからcreateStoreを使う -
Storeのstateをreactの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の定義をまとめるcombineReducersをreduxからインポートするcombineReducersでは、state内で扱うためのキー名とそのキーの値となるオブジェクトを返すreducerを指定するexport default combineReducers({ キー1: reducer1 キー2: reducer2 });
-
ルートComponentで
Store生成時にReducerを初期化する
-
-
actions/index.js-
stateに変化を与えるための関数Action Creator- 名前付きエクスポート(
named export)で関数名を決める
- 名前付きエクスポート(
-
変化させるタイプやその値の規格を定義したオブジェクト
Actiontypeとpayloadのセットにする
-
-
components/App.js- 制御する
Componentをインポートする
- 制御する
-
reduxでstateを使う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管理されているかどうかはrenderでconsole.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');
react-reduxのconnect()を図解する - Javaエンジニア、React+Redux+Firebaseでアプリを作る
- どうやら
CurryingというES6の書き方があるらしい
Redux-Thunk
役割
Action CreatorからAPIのリクエストを送り、レスポンスを受け取る- レスポンスの結果を
Actionのpayloadに格納する
書き方
-
index.js-
Storeを生成して使うため、reduxからcreateStoreを使う -
StoreのstateをreactのDOMの中でも使えるようにProviderを使う -
stateをどのように変化させるかの定義をしたreducerを初期化するため、インポートする -
reducerにActionを送る前に非同期処理をするミドルウェアをミドルウェアを追加しておくために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-thunkのcreateThunkMiddlewareにdispatchする-
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によるデータロード順
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の処理をbabeljsのTry it outでコピペして見ると想定してなかったオブジェクトがreturnされていることがわかる- 注意点として、
PRESETSの設定項目はすべてONにした状態で試す
- 注意点として、
-
なので、
async/awaitを使っても生のJavaScriptオブジェクトは帰って来ず、Actionで返すべきオブジェクトのタイプが合わない
-
-
非同期処理を使わずに同期的にデータをレスポンスしようとすると、
Action Creatorがリクエスト投げている間、Actionは先にdispatchでReducerまでたどり着いてしまう- そうすると、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 CreatorはActionオブジェクトもしくは関数を返す- 関数を返す場合、
関数の呼び出しもやってくれる
- 関数を返す場合、
-
Actionオブジェクトが返ってきたら必ずtypeを持つべき- 場合によっては
payloadもつけられる
- 場合によっては
-
redux-thunkの詳細な動き
<Action Creator>
↓
オブジェクトもしくは関数の「何か」
↓
※`dispatch`
↓
<redux-thunk内>
↓
「何か」
↓
関数かどうか? → <関数の場合> → `getState`と`dispatch`を使って関数の呼び出しをする → リクエストが終わるまで待つ → リクエストが終わったら、手動で`Action`を※`dispatch`する
↓
<関数ではない場合>
↓
Reducers
reducerのルール
返り値
- 「undefined」以外の値を返す必要がある
stateの取り扱い
-
「
state」、または以前のstateとアクションのみを使用してアプリ内で使用されるデータを生成する-
初期化時の
reducerはstateが何も決まっていない状態であるが、reducerに渡すときの引数をstate = nullのようにデフォルトバリューでundefined以外に指定することでエラー発生せず、初期化されたstateが生成できる -
初期以降は生成された
stateとactionをもとにreducerを通して新たなstateを生成する
-
Reducer内で扱える値について
-
どの値を返すかを決定するために、「Reducerが受け付けた値」以外で返してはなりません。
-
stateとactionで受け付けたもの以外にAPIリクエストを投げたり、ファイルの読み込みをしたり、DOMを直接介したりして返す値を決めてはいけない -
stateとactionのみで新しい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" - 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 !== 削除する値)
コメントする