前情提要:在前面關於 React 的幾篇文章,學會了建立 React Component,依據使用者操作在組件間處理資料。接下來就會提到如何使用 Redux,更方便的來管理應用程式中複雜化的資料狀態。
在 React 中,State 被用來儲存特定的資料狀態,網頁 UI會根據 State 狀態呈現出畫面 View,當使用者操作 Action 事件觸發資料狀態改變,網頁 UI 就會依據新的 State 更新網頁內容。
若有多個組件需要共享同一個 State,之前雖然提到可以使用提升 State 到最上層 Component,再經過 Props去傳遞的方法,但當網頁內容複雜化,管理的資料狀態增加後就很難去控制這些不斷變化的 State。
一個很好的解決方法就是將 State 提取到一個地方集中存放,並且指定 State 依據什麼樣的 Action 來轉變,就可以方便在 Component 間取用共用的資料狀態、和操作改變 State 的 Action,這就是 Redux 的作用。
Redux 是如何運作?在 Redux 中的所有資料都遵循一個單向資料流的模式,當使用者觸發操作事件,藉由 Dispatch Action 來讓 Store 啟動 Reducer,根據當前 State 和指定的 Action,產生一個新的 State 回傳,Store 再通知所有相關組件進行對應的更新。
在 Codesandbox 新增一個 React 專案,在左方的 Dependencies 安裝 Redux,不過我們目前還不會用到 React 的任何功能,下一篇才會講到如何在 React 專案中使用 Redux。
在 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 被用來描述會發生的操作事件,它是一個 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 會接收目前的 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.type
為 ADD_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
前面我們創建了 Action 與 Reducer,而 Store 的用處就是將它們組合在一起,保存 State、讓網頁能夠讀取更新狀態,而每一個 Redux 中只會有一個 Store。接下來的動作就是創建 Store,並引入 rootReducer
:
import { createStore } from "redux";
import rootReducer from "./reducer";
const store = createStore(rootReducer);
export default store;
建立好 Store 之後,在 index.js 引入 addScore
和 reduceScore
動作,並通過 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 來處理資料狀態,那就下一篇文章再見囉!
如果文章中有錯誤的地方,要麻煩各位大大不吝賜教;喜歡的話,也要記得幫我按讚訂閱喔❤️