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
)で関数名を決める
- 名前付きエクスポート(
-
変化させるタイプやその値の規格を定義したオブジェクト
Action
type
と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 !== 削除する値)
コメントする