iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Modern Web

重溫 React 官方文件回到最初的起點系列 第 27

Day 27 - 將 State 邏輯提取到 Reducer

  • 分享至 

  • xImage
  •  

隨著我們開發的程式碼規模變大,有時候在處理陣列的 state 的時候,所需要更新的邏輯也會越來越多,如果通通直接寫在 handler 裡面會讓我們 component 的程式碼變得龐大不好管理。這時候就會可以使用到 Reducer 這個工具幫我們各自的邏輯獨立起來使用,讓他們能專心處理他們那邊的邏輯。
今天的文章參考官方文件的:

在 React 使用 hook useReducer

Reducer 的概念有點像是所有的更新都會使用 dispatch 這個 function,然後他會透過 action 去找到相對應的更新邏輯,之後只要在 component 裡面 dispatch(action) 就能完成更新。

要在 React 裡面使用的話會把原本的 useState 改成 useReducer,並且照著三個步驟:

  1. 把原本的 state setter 改成 dispatch
  2. 寫 reducer 的邏輯 function
  3. 最後在 component 裡面使用 useReducer 來獲得 state 與 dispatch

1. 把原本的 state setter 改成 dispatch

首先介紹第一步驟,使用文章中的範例當例子,想要的功能是在畫面有 新增更新刪除 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。通常命名規則會用 _ 底線去分開,如果名字太長需要有中斷點的話。

2. 寫 reducer 的邏輯 function

有了 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 讓系統知道有問題產生。

3. 在 component 裡面使用 useReducer 來獲得 state 與 dispatch

寫完 reducer 後,我們就可以在 component 裡面用 useReducer 來使用它,寫法變為:

- const [tasks, setTasks] = useState(initialTasks);
+ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer 需要兩個參數,第一個是我們剛剛寫的 reducer,另一個則是 state 的初始值,之後他就會回傳設定的 state(這次是 tasks),跟用來更新 state 的 dispatch

比較 useStateuseReducer

照著上面的步驟就完成 reducer 的使用了,操作會完全跟之前用 useState 的結果一樣,但他們還是有一些不同的地方:

  • 程式碼大小:當邏輯還比較簡單的時候,使用 useState 可以寫得更簡潔,也不用特地多寫個 reducer,但當邏輯變雜 handler 變得塞太多東西的時候,用 useReducer 就能有效的減少寫在 component 裡面的程式碼。
  • 可讀性:跟前一點一樣,useReducer 也能在邏輯變複雜的時候讓 component 裡的程式碼減少,而且可以根據 action.type 去知道那段程式碼會做什麼事,比起直接在 handler 裡面看邏輯又更好讀了一些。但如果邏輯比較簡單,用 useState 會方便很多。
  • 測試與除錯:在前面也有稍微提到,因為 reducer 把各邏輯用 type 去標注與獨立的關係,要測試或是找到 bug 就去特定邏輯就好了,這也是 reducer 的優勢之一。

總體來說這兩個是都會用到的,不用特地去寫其中之一,太簡單的地方用 useReducer 會有點殺雞焉用牛刀,太複雜的地方用 useState 則是容易搞混,會根據當時的情境使用自己最習慣的寫法。

寫 reducers 注意事項

要注意的是寫 reducer 跟寫 useState 一樣,都是在更新 component 的 state,所以更新的 function 也要是純的,不能有 side effect。如果擔心的話 Immer 一樣有提供 hook useImmerReducer 可以使用,這樣也可以在 reducer 裡面用 draft 寫 side effect 邏輯了。
再來就是盡量讓每個 type 底下的邏輯都是獨立不互相影響的,並且命名要符合他裡面程式實際上做的,不要有叫 deleted 結果裡面還有新增東西。

小結

今天介紹了另一個在 component 裡更新 state 的用法 reducer 跟 useReducer,學會這個可以幫助我們更好的維護我們 component,大家可以多練習看看。
今天的文章就到這邊,感謝大家耐心地看完,如果有任何問題或建議歡迎留言告訴我,明天見,晚安。


上一篇
Day 26 - 保留和重置 State
下一篇
Day 29 - 使用 Context 深度傳遞資料
系列文
重溫 React 官方文件回到最初的起點28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言