Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Redux の利点を振り返る

Redux の利点を振り返る

readyfor_redux_study#1

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

Takepepe

June 29, 2020
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. Redux の基本

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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 処理の流れ

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  23. React.useReducer との違い

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  31. Global な文脈

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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 本来の挙動を忘れない様にしましょう(以下ドキュメント引用)

    View Slide

  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.

    View Slide

  53. React と 疎結合

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide