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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. Redux の基本

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  23. React.useReducer との違い

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  31. Global な文脈

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

  53. React と 疎結合

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide