Pro Yearly is on sale from $80 to $50! »

Redux の利点を振り返る

Redux の利点を振り返る

readyfor_redux_study#1

READYFOR株式会社 さんで開催した、Redux 勉強会第一回の資料です。

5d9cd19df0e91caac118b793b4f803d5?s=128

Takepepe

June 29, 2020
Tweet

Transcript

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

  2. Redux の利点 TL;DR ◼ イベント駆動 ◼ React と 疎結合 ◼

    エコシステムが豊富 ◼ 開発者母数が多い
  3. Redux のデータフロー Flux は一方向に処理が流れる アーキテクチャであり、 Redux は Flux 実装の一つです。 Action

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

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

  6. Redux の基本

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

  8. Action って何? ◼ Action は、ActionType と Payload が「対」になった Object ◼

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

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

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

    ActionType は、prj でユニークな識別子(文字列) ◼ Payload は、任意の値Object ◼ ActionCreator は「対」を生成するインターフェース cconst counterAdd = (amount) => ({ type: "COUNTER_ADD", // prj で重複しない識別子 payload: { amount } // 任意の値 })
  12. 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 } }
  13. 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 } }
  14. 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 } }
  15. 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 } }
  16. 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 } }
  17. 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 } }
  18. 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 } }
  19. 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 } }
  20. 興味のない 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 処理の流れ
  21. 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 } }
  22. 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 } }
  23. React.useReducer との違い

  24. React.useReducer との違い React の標準 API に、useReducer がありますが、 そこで利用する Reducer 関数も、

    Flux 実装であり処理の流れは同じです。 では、Redux との違いは何でしょうか?
  25. 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 } }
  26. 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 } }
  27. 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 } }
  28. 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 } }
  29. 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 } }
  30. 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 } }
  31. Global な文脈

  32. Global な文脈の何が嬉しいのか? Redux の場合「Action と Reducer 」は設計上疎結合です。 様々な Action が

    Global な文脈で発生し続けるので、 各々の Reducer は興味のあるものだけを購読し、 各々のユースケースに応じて、利用します。
  33. Global な文脈の何が嬉しいのか? どういった場面でこの性質が役に立つのか? 具体的に、プロダクトリリース・グロースを想像して、確認してみましょう。 UIHeader Reducer

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

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

  36. 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 } }
  37. Global な文脈の何が嬉しいのか? 後日、モーダルの機能が追加になりました。 モーダルの開閉状態を変更するのは ”UI_MODAL::TOGGLE” です。 UIModal Reducer UIHeader Reducer

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

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

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

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

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

    Reducer UIModal Reducer
  44. 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 } }
  45. Global な文脈の何が嬉しいのか? 他にも、外部 API を司るドメインが増えたなら。 データ取得・完了の Action も、全ての Reducer に伝搬します。

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

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

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

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

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

    UIModal Reducer Request Reducer PageA Reducer PageC Reducer PageB Reducer PageD Reducer Device Reducer
  51. 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 本来の挙動を忘れない様にしましょう(以下ドキュメント引用)
  52. 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.
  53. React と 疎結合

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

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

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

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

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

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

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

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