Slide 1

Slide 1 text

Redux の利点を振り返る readyfor_redux_study#1 @ Takepepe

Slide 2

Slide 2 text

Redux の利点 TL;DR ◼ イベント駆動 ◼ React と 疎結合 ◼ エコシステムが豊富 ◼ 開発者母数が多い

Slide 3

Slide 3 text

Redux のデータフロー Flux は一方向に処理が流れる アーキテクチャであり、 Redux は Flux 実装の一つです。 Action の発生を起点に、 処理が回ります。

Slide 4

Slide 4 text

この図には、Redux が 他の Flux実装と一線を画す、 最大の利点が載っていません。 今日はその点に絞って、 解説していきます。 Redux のデータフロー

Slide 5

Slide 5 text

Redux のデータフロー Redux Toolkit を使う事例が 昨今は多い様ですが、 今回は基本に立ち返り、一つ一つ 動きを見て行きましょう。

Slide 6

Slide 6 text

Redux の基本

Slide 7

Slide 7 text

◼ ActionType ◼ ActionCreator ◼ Reducer Redux のボイラープレート

Slide 8

Slide 8 text

Action って何? ◼ Action は、ActionType と Payload が「対」になった Object ◼ ActionType は、prj でユニークな識別子(文字列) ◼ Payload は、任意の値Object ◼ ActionCreator は「対」を生成するインターフェース const action = { type: "COUNTER_ADD", // prj で重複しない識別子 payload: { amount: 10 } // 任意の値 }

Slide 9

Slide 9 text

Action って何? ◼ Action は、ActionType と Payload が「対」になった Object ◼ ActionType は、prj でユニークな識別子(文字列) ◼ Payload は、任意の値Object ◼ ActionCreator は「対」を生成するインターフェース const action = { type: "COUNTER_ADD", // prj で重複しない識別子 payload: { amount: 10 } // 任意の値 }

Slide 10

Slide 10 text

Action って何? ◼ Action は、ActionType と Payload が「対」になった Object ◼ ActionType は、prj でユニークな識別子(文字列) ◼ Payload は、任意の値Object ◼ ActionCreator は「対」を生成するインターフェース const action = { type: "COUNTER_ADD", // prj で重複しない識別子 payload: { amount: 10 } // 任意の値 }

Slide 11

Slide 11 text

Action って何? ◼ Action は、ActionType と Payload が「対」になった Object ◼ ActionType は、prj でユニークな識別子(文字列) ◼ Payload は、任意の値Object ◼ ActionCreator は「対」を生成するインターフェース cconst counterAdd = (amount) => ({ type: "COUNTER_ADD", // prj で重複しない識別子 payload: { amount } // 任意の値 })

Slide 12

Slide 12 text

State はどこにある? Reducer と呼ばれる関数が、状態(State)を抱えています。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 13

Slide 13 text

State はどこにある? Reducer と呼ばれる関数が、状態(State)を抱えています。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 14

Slide 14 text

State はどこにある? 状態(State)を変更できるのは、Reducer だけです。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 15

Slide 15 text

Reducer 処理の流れ dispatch(action) の度に、Reducer の処理は実行されます。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 16

Slide 16 text

Reducer 処理の流れ まずは Action を受け取ります。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 17

Slide 17 text

Reducer 処理の流れ Action は、様々なものが降ってきます。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 18

Slide 18 text

Reducer 処理の流れ ActionType で、興味のある Action を選り分けます。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 19

Slide 19 text

Reducer 処理の流れ 興味のある Action が発生した場合、値を更新した新しい state を返します。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 20

Slide 20 text

興味のない Action が発生した場合、そのまま state を返却します。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } } Reducer 処理の流れ

Slide 21

Slide 21 text

Reducer 処理の流れ 返却された state は、次回 Action 発生時の第一引数になります。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 22

Slide 22 text

Reducer 処理の流れ これが Reducer 処理の一連です。Action を選り分け、必ず state を返却します。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "COUNTER_ADD": const count = state.count + action.payload.amuont return { ...state, count } default: return state } }

Slide 23

Slide 23 text

React.useReducer との違い

Slide 24

Slide 24 text

React.useReducer との違い React の標準 API に、useReducer がありますが、 そこで利用する Reducer 関数も、 Flux 実装であり処理の流れは同じです。 では、Redux との違いは何でしょうか?

Slide 25

Slide 25 text

React.useReducer との違い React.useReducer の場合、特定の State に向けて、 特定の Action が発行される事を想定し、定義します。 const reducer = (state = { count: 0 }, action) => { switch (action.type) { case "INCREMENT": return { ...state, count: state.count + 1 } case "DECREMENT": return { ...state, count: state.count - 1 } default: return state } }

Slide 26

Slide 26 text

React.useReducer との違い 一方で、Redux の場合、特定の State に向けて、 特定の Action が発行されるとは限りません。 const reducerA = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return { ...state, count: state.count + 1 } case "B:INCREMENT": return state default: return state } } const reducerB = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return state case "B:INCREMENT": return { ...state, count: state.count + 1 } default: return state } }

Slide 27

Slide 27 text

React.useReducer との違い Redux の Reducer は、複数の Reducer を 「一つの Reducer として連結」することが出来るのです。 const reducerA = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return { ...state, count: state.count + 1 } case "B:INCREMENT": return state default: return state } } const reducerB = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return state case "B:INCREMENT": return { ...state, count: state.count + 1 } default: return state } }

Slide 28

Slide 28 text

React.useReducer との違い reducerA に向けて、意識的に発行した Action であっても、 reducerB に勝手に到達します。(逆も然り) const reducerA = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return { ...state, count: state.count + 1 } case "B:INCREMENT": return state default: return state } } const reducerB = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return state case "B:INCREMENT": return { ...state, count: state.count + 1 } default: return state } }

Slide 29

Slide 29 text

React.useReducer との違い Redux の場合 Action / Reducer 共に、Global な文脈に属します。 「ActionType が一意な識別子」でなければいけないのは、このためです。 const reducerA = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return { ...state, count: state.count + 1 } case "B:INCREMENT": return state default: return state } } const reducerB = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return state case "B:INCREMENT": return { ...state, count: state.count + 1 } default: return state } }

Slide 30

Slide 30 text

Redux Toolkit の Slice を利用すると、この挙動に気づきにくくなっています。 この挙動を利用する設計パターンが、Redux の強みとも言えます。 Redux Toolkit / createSlice に注意 const reducerA = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return { ...state, count: state.count + 1 } case "B:INCREMENT": return state default: return state } } const reducerB = (state = { count: 0 }, action) => { switch (action.type) { case "A:INCREMENT": return state case "B:INCREMENT": return { ...state, count: state.count + 1 } default: return state } }

Slide 31

Slide 31 text

Global な文脈

Slide 32

Slide 32 text

Global な文脈の何が嬉しいのか? Redux の場合「Action と Reducer 」は設計上疎結合です。 様々な Action が Global な文脈で発生し続けるので、 各々の Reducer は興味のあるものだけを購読し、 各々のユースケースに応じて、利用します。

Slide 33

Slide 33 text

Global な文脈の何が嬉しいのか? どういった場面でこの性質が役に立つのか? 具体的に、プロダクトリリース・グロースを想像して、確認してみましょう。 UIHeader Reducer

Slide 34

Slide 34 text

Global な文脈の何が嬉しいのか? UIHeader Reducer は、Header ナビゲーションの開閉状態を保持してる、 GlobalUI 専用の Reducer です。 UIHeader Reducer

Slide 35

Slide 35 text

Global な文脈の何が嬉しいのか? 開閉状態を変更できるのは、”UI_HEADER::TOGGLE” という Action が発生した時です。この機能は無事、リリースを迎えました。 UIHeader Reducer

Slide 36

Slide 36 text

Global な文脈の何が嬉しいのか? 開閉状態を変更できるのは、”UI_HEADER::TOGGLE” という Action が発生した時です。この機能は無事、リリースを迎えました。 UIHeader Reducer const UIHeaderReducer = (state = { open: false }, action) => { switch (action.type) { case "UI_HEADER::TOGGLE": return { ...state, open: action.payload.open } default: return state } }

Slide 37

Slide 37 text

Global な文脈の何が嬉しいのか? 後日、モーダルの機能が追加になりました。 モーダルの開閉状態を変更するのは ”UI_MODAL::TOGGLE” です。 UIModal Reducer UIHeader Reducer

Slide 38

Slide 38 text

Global な文脈の何が嬉しいのか? そして「モーダルが開いている時、ヘッダーは閉じて欲しい」という 既存の機能(ヘッダー)に要件が追加されました。 UIModal Reducer UIHeader Reducer

Slide 39

Slide 39 text

Global な文脈の何が嬉しいのか? 手続き的な ”UI_HEADER::TOGGLE” の発行を、はじめに思いつくでしょう。 しかし、もっと簡単な方法があります。 UIModal Reducer UIHeader Reducer

Slide 40

Slide 40 text

Global な文脈の何が嬉しいのか? ”UI_MODAL::TOGGLE” の Action を UIHeader Reducer に引き込み、 「閉じた状態」にするアプローチです。 UIModal Reducer UIHeader Reducer

Slide 41

Slide 41 text

Global な文脈の何が嬉しいのか? Global な文脈がここで活きます。”UI_MODAL::TOGGLE” が発生したら、 UIHeader Reducer は必ず「閉じる」状態にすれば良いだけです。 UIModal Reducer UIHeader Reducer const UIHeaderReducer = (state = { open: false }, action) => { switch (action.type) { case "UI_HEADER::TOGGLE": return { ...state, open: action.payload.open } case "UI_MODAL::TOGGLE": if (!state.open) return state return { ...state, open: false } default: return state } }

Slide 42

Slide 42 text

Global な文脈の何が嬉しいのか? 更に、実装が進み「画面遷移時に、全部閉じて欲しい」 という機能要件が追加されました。 UIHeader Reducer Router Reducer UIModal Reducer

Slide 43

Slide 43 text

Global な文脈の何が嬉しいのか? これも簡単ですね。”ROUTER::LOCATION_CHANGE” が発生したら、 各々の UIReducer が「閉じる」状態に変更すれば良いのです。 UIHeader Reducer Router Reducer UIModal Reducer

Slide 44

Slide 44 text

Global な文脈の何が嬉しいのか? これも簡単ですね。”ROUTER::LOCATION_CHANGE” が発生したら、 各々の UIReducer が「閉じる」状態に変更すれば良いのです。 UIHeader Reducer Router Reducer UIModal Reducer const UIHeaderReducer = (state = { open: false }, action) => { switch (action.type) { case "UI_HEADER::TOGGLE": return { ...state, open: action.payload.open } case "UI_MODAL::TOGGLE": if (!state.open) return state return { ...state, open: false } case "ROUTER::LOCATION_CHANGE": if (!state.open) return state return { ...state, open: false } default: return state } }

Slide 45

Slide 45 text

Global な文脈の何が嬉しいのか? 他にも、外部 API を司るドメインが増えたなら。 データ取得・完了の Action も、全ての Reducer に伝搬します。 UIHeader Reducer Router Reducer UIModal Reducer Request Reducer

Slide 46

Slide 46 text

Global な文脈の何が嬉しいのか? 他にも、外部 API を司るドメインが増えたなら。 データ取得・完了の Action も、全ての Reducer に伝搬します。 UIHeader Reducer Router Reducer UIModal Reducer Request Reducer

Slide 47

Slide 47 text

Global な文脈の何が嬉しいのか? データ取得に失敗したのなら。ユーザーに通知する必要がありますね。 モーダルを開いて通知することも、難なくこなせそうです。 UIHeader Reducer Router Reducer UIModal Reducer Request Reducer

Slide 48

Slide 48 text

イベント駆動はスケーラブル デバイスイベント・ページの状態・WebSocket イベント… 将来どの様な機能が追加されるのか、誰にも分かりませんね。 UIHeader Reducer Router Reducer UIModal Reducer Request Reducer PageA Reducer PageC Reducer PageB Reducer PageD Reducer Device Reducer

Slide 49

Slide 49 text

イベント駆動はスケーラブル しかし、Action という「抽象化インターフェース」があれば、 各々のユースケースに併せ、内側の関心のみに変化を与える事ができます。 UIHeader Reducer Router Reducer UIModal Reducer Request Reducer PageA Reducer PageC Reducer PageB Reducer PageD Reducer Device Reducer

Slide 50

Slide 50 text

これが「イベント駆動」と呼ばれる、Redux の特徴です。 ユースケース単位で、カジュアルに Reducer を生やす事が出来ます。 イベント駆動はスケーラブル UIHeader Reducer Router Reducer UIModal Reducer Request Reducer PageA Reducer PageC Reducer PageB Reducer PageD Reducer Device Reducer

Slide 51

Slide 51 text

Redux Toolkit / createSlice に注意 アクションは、単一のスライスに限定されません。 レデューサーロジックのどの部分も、 ディスパッチされたアクションに応答できます(そして、そうすべきです!)。 Actions are not exclusively limited to a single slice. Any part of the reducer logic can (and should!) respond to any dispatched action. createSlice では、この設計パターンに気づきにくくなっているため、 Reuducer 本来の挙動を忘れない様にしましょう(以下ドキュメント引用)

Slide 52

Slide 52 text

Redux Toolkit / createSlice に注意 実現するためには、extraReducers を利用する必要があります。 extra というニュアンスですが、本来の挙動なので積極的に活用しましょう。 アクションは、単一のスライスに限定されません。 レデューサーロジックのどの部分も、 ディスパッチされたアクションに応答できます(そして、そうすべきです!)。 Actions are not exclusively limited to a single slice. Any part of the reducer logic can (and should!) respond to any dispatched action.

Slide 53

Slide 53 text

React と 疎結合

Slide 54

Slide 54 text

React と疎結合である利点 Redux と聞くと「React の状態管理ライブラリ」というイメージが強いですが、 「Redux は React と疎結合」であるという利点があります。

Slide 55

Slide 55 text

接続するために「react-redux」という package を利用します。 React Hooks が登場した時も、影響を受けたのはこの package だけです。 React と疎結合である利点 react-redux

Slide 56

Slide 56 text

ランタイムで 同一の Store インスタンスを捉えられれば、 Action はどこからでも発行することが可能です。 React と疎結合である利点

Slide 57

Slide 57 text

例えば jQuery + HTML からも、Action を発行することが出来ます。 Store の変化は当然、React Component にも伝搬します。 React と疎結合である利点

Slide 58

Slide 58 text

View 技術が異なっていても、状態の共有が可能であり、 React Hooks 登場の様な、View 都合の影響も最小限に留まります。 React と疎結合である利点

Slide 59

Slide 59 text

2020年の今なお Redux を選ぶ理由は、 「状態管理」を独立した実装とするためです。 React Hooks を利用した設計議論とは、 少し論点が異なります。

Slide 60

Slide 60 text

「データソース・環境副作用・UI副作用」 これらを Action という「抽象化インターフェース」で 統合可能なことが最大の利点です。 ただの巨大なシングルトンではありません。

Slide 61

Slide 61 text

データストアとしてだけではなく、 「アプリケーションの状態管理」として 積極的に活用していきましょう。