iT邦幫忙

2021 iThome 鐵人賽

DAY 22
1
Modern Web

Follow me! 來一場前端技能樹之旅系列 第 22

[Day 22 - Redux] 有了Redux,狀態管理沒煩惱

  • 分享至 

  • xImage
  •  

前情提要:在前面關於 React 的幾篇文章,學會了建立 React Component,依據使用者操作在組件間處理資料。接下來就會提到如何使用 Redux,更方便的來管理應用程式中複雜化的資料狀態。

為什麼要使用 Redux ?

在 React 中,State 被用來儲存特定的資料狀態,網頁 UI會根據 State 狀態呈現出畫面 View,當使用者操作 Action 事件觸發資料狀態改變,網頁 UI 就會依據新的 State 更新網頁內容。

若有多個組件需要共享同一個 State,之前雖然提到可以使用提升 State 到最上層 Component,再經過 Props去傳遞的方法,但當網頁內容複雜化,管理的資料狀態增加後就很難去控制這些不斷變化的 State。

一個很好的解決方法就是將 State 提取到一個地方集中存放,並且指定 State 依據什麼樣的 Action 來轉變,就可以方便在 Component 間取用共用的資料狀態、和操作改變 State 的 Action,這就是 Redux 的作用。

Redux

Redux 是如何運作?在 Redux 中的所有資料都遵循一個單向資料流的模式,當使用者觸發操作事件,藉由 Dispatch Action 來讓 Store 啟動 Reducer,根據當前 State 和指定的 Action,產生一個新的 State 回傳,Store 再通知所有相關組件進行對應的更新。

安裝 Redux

在 Codesandbox 新增一個 React 專案,在左方的 Dependencies 安裝 Redux,不過我們目前還不會用到 React 的任何功能,下一篇才會講到如何在 React 專案中使用 Redux。

定義 State

在 Redux 中的 State 會是一個單一的物件存放在 Store,每次發生狀態更新時,都要參考先前的狀態來產生一個新的 State,所以我們需要先定義一個最初始的 State,讓網頁內容能夠依據 State 來產生畫面。延續之前的計數器範例來舉例,新增 reducer.js 定義 initialState,包含每個成員的姓名和分數資料:

reducer.js

const initialState = {
  members: [
    { name: "May", score: 0 },
    { name: "Julia", score: 0 },
    { name: "Selina", score: 0 }
  ]
};

Action

Action 被用來描述會發生的操作事件,它是一個 JavaScript 物件,包含 type 屬性指定 Action 類型、和 payload 屬性定義動作所需的額外數據。比如,我們定義一個按下 Plus 按鈕的動作 ADD_SCORE,並且取得該成員的編號索引 idx,知道是針對哪位成員的分數進行操作。

// 添加一個新的 action type:
export const ADD_SCORE = "ADD_SCORE";
// 定義一個 action
{
  type: ADD_SCORE,
  payload: { idx }
}

接下來一併定義另一個動作 REDUCE_SCORE,而通常會建立一個 Action Creator 產生該動作的 Function,回傳需要的指定 Action 供 Dispatch 使用,如下面的程式碼所示:

action.js

export const ADD_SCORE = "ADD_SCORE";
export const REDUCE_SCORE = "REDUCE_SCORE";

// action creator
export function addScore(idx) {
  return {
    type: ADD_SCORE,
    payload: { idx }
  };
}

export function reduceScore(idx) {
  return {
    type: REDUCE_SCORE,
    payload: { idx }
  };
}

Reducer

Reducer 會接收目前的 State 和指定的一個 Action,根據這兩個參數計算出下一個 State 並回傳給 Store。首先創建一個 Reducer,state 參數的預設值為之前定義的 initialState;傳進來的 action 尚未定義時,就回傳當前原本的 State:

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    default:
      return state;
  }
}

先來處理 ADD_SCORE 動作,讀取 action.typeADD_SCORE 時,執行分數加一的動作,計算出要回傳的新 State。這邊要注意的是,Reducer 不允許我們直接修改當前的 state,我們只能通過複製該值來修改

reducer.js

import { ADD_SCORE, REDUCE_SCORE } from "./actions";

const initialState...

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_SCORE: {
      let new_members = state.members;
      new_members[action.payload.idx] = {
        ...new_members[action.payload.idx],
        score: new_members[action.payload.idx].score + 1
      };
     // 回傳一份新的state object
      return {
        members: new_members
      };
    }
    default:
      ...
  }
}

然後是 REDUCE_SCORE 動作的部分:

...
  switch (action.type) {
    case ADD_SCORE: 
        ...
    case REDUCE_SCORE: {
      let new_members = state.members;
      new_members[action.payload.idx] = {
        ...new_members[action.payload.idx],
        score: new_members[action.payload.idx].score - 1
      };
      return {
        members: new_members
      };
    }
    default:
      ...
  }
}

Redux 實際上只會有一個 Reducer 函數,但我們還是可以將 State 拆分給好幾個 Reducer 來控制,透過 combineReducers 合併成一個 rootReducer,讓 Reducer 只處理與各自相關的 State 更新動作。

const initialState2...

export default function Reducer2(state = initialState2, action) {
  switch (action.type) {
      ...
  }
}

透過 combineReducers 合併組合多個 Reducer:

import { combineReducers } from 'redux'

import reducer1 from ...
import reducer2 from ...

const rootReducer = combineReducers({
  r1: reducer1,
  r2: reducer2
})

export default rootReducer

Store

前面我們創建了 Action 與 Reducer,而 Store 的用處就是將它們組合在一起,保存 State、讓網頁能夠讀取更新狀態,而每一個 Redux 中只會有一個 Store。接下來的動作就是創建 Store,並引入 rootReducer

import { createStore } from "redux";
import rootReducer from "./reducer";

const store = createStore(rootReducer);

export default store;

Dispatching Actions

建立好 Store 之後,在 index.js 引入 addScorereduceScore 動作,並通過 Store 提供的方法 store.dispatch() 來調用 Action,就能呼叫對應的 Reducer Function 更新 State。

index.js

import { addScore , reduceScore } from "./actions";
import store from "./store";

// 觸發addScore,對編號索引 idx = 1 (第二位成員)執行分數+1的動作
store.dispatch(addScore(1));

這樣就完成了 Redux 完整的運作,我們另外使用一些 Store 提供的方法來查看更新後的 State 結果。

  • store.getState():取得目前的 State狀態
  • store.subscribe(listener):註冊 Listener,每次 Dispatch Action 時會呼叫,並且會回傳一個用來撤銷 Listener 的 Function。

index.js

import { addScore , reduceScore } from "./actions";
import store from "./store";

const unsubscribe = store.subscribe(() =>
  console.log("State after dispatch: ", store.getState())
);

store.dispatch(addScore(1));

// 撤銷 listener
unsubscribe();

成功將第二位成員的分數+1


小結

學到了如何在 Redux 透過 Action 管理更新所有的 State,但我們還沒有將 State 放到網頁元件裡渲染出對應的畫面,所以接下來,就會談到如何在 React 中使用 Redux 來處理資料狀態,那就下一篇文章再見囉!

範例程式碼

如果文章中有錯誤的地方,要麻煩各位大大不吝賜教;喜歡的話,也要記得幫我按讚訂閱喔❤️

參考資料


上一篇
[Day 21 - React] 今晚我想來點,React的其他功能
下一篇
[Day 23 - Redux] React + Redux = React-redux
系列文
Follow me! 來一場前端技能樹之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言