隨著我們開發的程式碼規模變大,有時候在處理陣列的 state 的時候,所需要更新的邏輯也會越來越多,如果通通直接寫在 handler
裡面會讓我們 component 的程式碼變得龐大不好管理。這時候就會可以使用到 Reducer 這個工具幫我們各自的邏輯獨立起來使用,讓他們能專心處理他們那邊的邏輯。
今天的文章參考官方文件的:
useReducer
Reducer 的概念有點像是所有的更新都會使用 dispatch
這個 function,然後他會透過 action
去找到相對應的更新邏輯,之後只要在 component 裡面 dispatch(action)
就能完成更新。
要在 React 裡面使用的話會把原本的 useState
改成 useReducer
,並且照著三個步驟:
dispatch
useReducer
來獲得 state 與 dispatchdispatch
首先介紹第一步驟,使用文章中的範例當例子,想要的功能是在畫面有 新增、更新、刪除 task
的功能,所以寫在 handler 裡面變成:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
第一步就是要把這些 setTasks
改成 dispatch
:
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
在使用 dispatch
的時候,會傳入一個 object action
,裡面都一定要放一個 type
,這個是為了要讓 reducer 辨別要使用哪個邏輯的類似標籤的變數,而後面的則是會根據邏輯需要的所傳入的參數 paylod
,像是 deleted
會需要 taskId
當作找到需刪除的 id
的值。
type
的命名很重要,這除了讓程式判別要用哪種邏輯我們自己在閱讀維護程式碼的時候,也比較能清楚知道這段程式碼是在做什麼,出現 bug 的時候該找哪個 type。通常命名規則會用 _
底線去分開,如果名字太長需要有中斷點的話。
有了 dispatch
之後,我們就需要來寫 reducer 裡的邏輯,這次的功能有三種,新增更改刪除,然後在前面提到 reducer 會需要知道 action.type
去判別要使用哪段邏輯,所以可以寫成這樣:
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
tasks
就是我們原本寫在 useState
裡面的 state,action 則是我們從 dispatch
傳入的值。通常我們自己開發時寫 reducer 都習慣用 switch 當我們的判斷式,記得當傳入的 type
是不認識的可以拋出 Error 讓系統知道有問題產生。
useReducer
來獲得 state 與 dispatch寫完 reducer 後,我們就可以在 component 裡面用 useReducer
來使用它,寫法變為:
- const [tasks, setTasks] = useState(initialTasks);
+ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
需要兩個參數,第一個是我們剛剛寫的 reducer,另一個則是 state 的初始值,之後他就會回傳設定的 state(這次是 tasks
),跟用來更新 state 的 dispatch
。
useState
與 useReducer
照著上面的步驟就完成 reducer 的使用了,操作會完全跟之前用 useState
的結果一樣,但他們還是有一些不同的地方:
useState
可以寫得更簡潔,也不用特地多寫個 reducer,但當邏輯變雜 handler 變得塞太多東西的時候,用 useReducer
就能有效的減少寫在 component 裡面的程式碼。useReducer
也能在邏輯變複雜的時候讓 component 裡的程式碼減少,而且可以根據 action.type
去知道那段程式碼會做什麼事,比起直接在 handler 裡面看邏輯又更好讀了一些。但如果邏輯比較簡單,用 useState
會方便很多。type
去標注與獨立的關係,要測試或是找到 bug 就去特定邏輯就好了,這也是 reducer 的優勢之一。總體來說這兩個是都會用到的,不用特地去寫其中之一,太簡單的地方用 useReducer
會有點殺雞焉用牛刀,太複雜的地方用 useState
則是容易搞混,會根據當時的情境使用自己最習慣的寫法。
要注意的是寫 reducer 跟寫 useState
一樣,都是在更新 component 的 state,所以更新的 function 也要是純的,不能有 side effect。如果擔心的話 Immer 一樣有提供 hook useImmerReducer
可以使用,這樣也可以在 reducer 裡面用 draft
寫 side effect 邏輯了。
再來就是盡量讓每個 type 底下的邏輯都是獨立不互相影響的,並且命名要符合他裡面程式實際上做的,不要有叫 deleted
結果裡面還有新增東西。
今天介紹了另一個在 component 裡更新 state 的用法 reducer 跟 useReducer
,學會這個可以幫助我們更好的維護我們 component,大家可以多練習看看。
今天的文章就到這邊,感謝大家耐心地看完,如果有任何問題或建議歡迎留言告訴我,明天見,晚安。