iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 14
3

前言

在上一篇的資料處理篇中,我們學習到了如何將資料放到 Store 中儲存,並以 useSelector 將資料取出,而本篇會提及如何使用 Reducer 異動資料。


前置準備

  1. 文中的專案會以 Day12 的專案架構繼續講解,如果未跟到前一天的進度,可以從 GitHub 上 Clone 下來。
  2. 一顆擁有學習熱忱的心。

使用方法

建立 Action

首先打開專案裡的 ./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 的物件

Reducer 是靠第二個參數 actiontype 去判斷要做什麼事情,因此我們得把剛剛的 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 處理事件有什麼不同嗎?有的!

因為保管於 Reducer 內的值,就是靠 Store 以 Props 的方式傳遞給 Component 的。

但那又如何?

關鍵在 JavaScript 的 Object 型別的變數,都是參考址,而不是值。

這裡舉個簡單的小例子,假設我們擁有陣列 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 與它們組合成一個新的陣列,如此一來他也會擁有新的址。

useDispatch

要觸發 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,且將它解釋得更清楚,對我來說也算是一種成長,其實還滿欣慰的啦!

如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!


上一篇
Day12 | React 的快樂小夥伴 - Redux 資料管理篇
下一篇
Day14 | Redux 的改變,Logger 看得見
系列文
在 React 生態圈內打滾的一年 feat. TypeScript31

尚未有邦友留言

立即登入留言