在上一篇的資料處理篇中,我們學習到了如何將資料放到 Store 中儲存,並以 useSelector
將資料取出,而本篇會提及如何使用 Reducer 異動資料。
首先打開專案裡的 ./src/reducer/todolist.js:
const initState = {
todoList: ['第一件事情', '第二件事情'],
};
const todoReducer = (state = initState, action) => {
switch (action.type) {
default:
return state;
}
};
export default todoReducer;
記得上一篇說過,
第二個參數會傳入現在 Reducer 要對 State 做什麼動作的指令及額外的參數,
在 Reducer 內會根據 action.type
來判斷要做什麼事情,假設我們要做新增待辦事項的功能,就得先為這個事件創建指令,而創建的指令會需要一個新的目錄 action 來管理:
建立新目錄 action,以及管理觸發 todoReducer
指令的檔案 todolist.js :
|- src
|- action
|- todolist.js
開啟 src/action/todolist.js,並宣告一個與指令同名的常數變數:
export const ADD_TODO = 'ADD_TODO';
記得將指令 export
,其他地方有需要使用到就從這裡 import
,如此一來就能確保 action 指令的來源只有一個,而變數名稱全大寫以分辨為常數,雖然已有 const
,但這麼寫會讓語意更清楚。
Reducer 是靠第二個參數 action
的 type
去判斷要做什麼事情,因此我們得把剛剛的 ADD_TODO
放到一個物件的 type
屬性裡,像這樣子:
{ type: ADD_TODO, }
接下來,因為是新增待辦事項,所以物件裡總得有放新的待辦事項的地方,通常送給 Reducer 執行事件時的參數,都會放在 payload
屬性中,因此物件會變成:
{
type: ADD_TODO,
payload: {
todo: '新的事件',
},
}
當然在程式裡不可能寫死的,我們需要能夠依照使用者輸入的值來傳入新的待辦事項名稱,因此要包裝一個 Function,然後傳入待辦事項的名稱作為參數,產生能夠觸發 Reducer 的物件:
const addTodo = todo => ({
type: ADD_TODO,
payload: {
todo,
},
});
這麼一來只要呼叫 addTodo
,並傳入待辦事項的名稱,就能夠回傳觸發 Reducer 的物件了,而這個 Method 在稍後會由 Component 內的 DOM 使用,因此也要將它做 export。
把指令處理完後,再回到 src/reducer/todolist.js,我們需要在 switch...case
中增加新的分支 ADD_TODO
,注意這裡別再另外建立或手打字串 ADD_TODO
,直接從 src/action/todolist.js 取來用就好,否則統一管理就沒意義了 :
import * as actions from '../action/todolist';
/* 其餘程式碼省略 */
const todoReducer = (state = initState, action) => {
switch (action.type) {
case actions.ADD_TODO:
return state;
default:
return state;
}
};
/* 其餘程式碼省略 */
那就現階段來說,我們加入了另一個 case
,讓 action.type
的值如果是 ADD_TODO
的話,可以被另外處理,儘管它現在仍然是回傳一個沒有經過任何動作處理的 state
,這裡我覺得是很需要注意的部分,因此我將另外提出來說明。
React 是個很聰明的框架,它能夠判斷何時該讓 Component 重新 Render,一是 Props,另一個是 State 當這兩個元素,有其中一個的值發生變化,那 Component 就會重新 Render 新的畫面。
那這和 Reducer 處理事件有什麼不同嗎?有的!
但那又如何?
這裡舉個簡單的小例子,假設我們擁有陣列 todolist
,但我僅僅是使用 push
替它增加值,雖然
todolist
, 址是沒有變化的!導致就算資料內真的改變,也會因為址沒有變化,所以 Component 不會重新 Render。
怎麼辦?
詳細的用法可以看 擴展運算子 和 解構賦值 ,本篇就不描述了。
總之 todoReducer
內的 ADD_TODO
不能這麼寫:
// todoList 的址不會有變化
case actions.ADD_TODO:
state.todoList.push(action.payload.todo);
return state;
但配合上述技巧,你可以這麼做:
case actions.ADD_TODO:
return {
...state,
todoList: [
...state.todoList,
action.payload.todo,
],
};
把 state.todoList
的內容拆開,再將 action.payload.todo
與它們組合成一個新的陣列,如此一來他也會擁有新的址。
要觸發 todoReducer
總不可能直接呼叫 todoReducer
,因為它是由 Store 所管理的,所以我們得先以 useDispatch
取得 dispatch
後,再對 dispatch
傳入對應的 Action 物件,就能透過 Store 代為觸發,改變 Reducer 內所管理的 State,Component 也會重新 Render。
開啟 src/index.tsx 在最上方 import
useDispatch
和產生觸發 Reducer 物件的 Function:
import { Provider, useSelector, useDispatch } from 'react-redux';
import { addTodo } from './action/todolist';
然後我把輸入新事件的輸入框和新增的按鈕放在 Main
Component 中,並使用 State 管理 input
內輸入的值:
再來,在 Component 中以 useDispatch
取得 dispatch
,並在新增按鈕上加上 onClick
事件,點擊後執行 dispatch
,並在 dispatch
內傳入使用 addTodo
產生的 Action 物件,別忘了 State 裡的 newTodo
也要給 addTodo
,否則 todoReducer
讀到的 action.payload.todo
就會是 undefined
:
我們已經從 Action、Reducer 到 Component 都做好準備了,現在可以運行 npm run start
指令確認執行狀況:
本文的範例程式碼會提供在 GitHub 上,歡迎各位參考:)
本篇幾乎講解了 Redux 在使用時的 Know How,其中最容易踩雷的就是 Reference,也就是參考址的問題,其實在去年學習 Redux 的時候,根本就不曉得為什麼要用解構賦值寫,詳情可以看去年鐵人賽的 這篇文章 ,實在是滿慘不忍睹的,但一年後試著重新詮釋 Redux,且將它解釋得更清楚,對我來說也算是一種成長,其實還滿欣慰的啦!
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
回顧去年神Q的Redux三篇文章,真的是有看沒有懂,最後只知道,oh!React有個東西叫Redux經常會一起用XDDD。
今天看完總算更懂一些了!感謝您。
我當時真的不知道在寫什麼,非常抱歉 XD
是我沒有慧根,沒辦法好好的意會。就像神Q說的相信努力不會白費,一遍不懂再看一遍XDD