v19.2Latest

將狀態邏輯提取到 Reducer 中

具有許多狀態更新且分散在許多事件處理器中的元件可能會變得難以管理。對於這些情況,您可以將所有狀態更新邏輯整合到元件外部的一個單一函式中,稱為reducer。

您將學習
  • 什麼是 reducer 函式
  • 如何將 useState重構為useReducer
  • 何時使用 reducer
  • 如何寫好一個 reducer

使用 Reducer 整合狀態邏輯

隨著您的元件變得越來越複雜,可能很難一眼看出元件狀態的所有不同更新方式。例如,下面的 TaskApp元件在狀態中保存了一個tasks陣列,並使用三個不同的事件處理器來新增、刪除和編輯任務:

每個事件處理器都會呼叫setTasks來更新狀態。隨著這個元件的增長,散佈其中的狀態邏輯量也會增加。為了降低這種複雜性並將所有邏輯集中在一個易於存取的地方,您可以將該狀態邏輯移到元件外部的一個單一函式中,稱為「reducer」。

Reducer 是處理狀態的另一種方式。您可以透過三個步驟從useState遷移到useReducer

  1. 設定狀態轉為派發動作。
  2. 撰寫一個 reducer 函式。
  3. 在你的元件中使用該 reducer。

步驟 1:從設定狀態轉為派發動作

你的事件處理器目前透過設定狀態來指定要做什麼

移除所有設定狀態的邏輯。你剩下的是三個事件處理器:

  • handleAddTask(text)在使用者按下「新增」時呼叫。
  • handleChangeTask(task)在使用者切換任務或按下「儲存」時呼叫。
  • handleDeleteTask(taskId)在使用者按下「刪除」時呼叫。

使用 reducer 管理狀態與直接設定狀態略有不同。你不是透過設定狀態來告訴 React「要做什麼」,而是透過從事件處理器派發「動作」來指定「使用者剛剛做了什麼」。(狀態更新邏輯將存在別處!)因此,你不是透過事件處理器「設定tasks」,而是派發一個「新增/變更/刪除任務」的動作。這更能描述使用者的意圖。

你傳遞給dispatch的物件稱為一個「動作」:

它是一個普通的 JavaScript 物件。你可以決定在其中放入什麼,但通常它應該包含關於發生了什麼的最少資訊。(你將在後續步驟中新增dispatch函式本身。)

注意

一個動作物件可以具有任何形狀。

按照慣例,通常會給它一個字串 type 來描述發生了什麼事,並在其他欄位中傳遞任何額外資訊。type是針對特定元件的,所以在這個例子中,'added''added_task'都可以。選擇一個能說明發生了什麼事的名稱!

步驟 2:編寫一個 reducer 函數

reducer 函數是你放置狀態邏輯的地方。它接受兩個參數:當前狀態和動作物件,並回傳下一個狀態:

React 會將狀態設定為你從 reducer 返回的值。

要將你的狀態設定邏輯從事件處理器移動到此範例中的 reducer 函數,你需要:

  1. 將當前狀態(tasks)宣告為第一個參數。
  2. action物件宣告為第二個參數。
  3. 從 reducer 返回下一個狀態(React 將把狀態設定為此值)。

以下是所有遷移到 reducer 函數的狀態設定邏輯:

由於 reducer 函數將狀態(tasks)作為參數,你可以在元件外部宣告它。這減少了縮排層級,並能使你的程式碼更易於閱讀。

注意

上面的程式碼使用了 if/else 語句,但在 reducer 內部使用switch 語句是一種慣例。結果相同,但 switch 語句通常更易於一目瞭然地閱讀。

在本文件的其餘部分,我們將像這樣使用它們:

我們建議將每個 case區塊包裹在{} 大括號中,這樣在不同 case中宣告的變數就不會互相衝突。此外,一個case 通常應該以 return結束。如果你忘記return,程式碼將會「貫穿」到下一個case,這可能導致錯誤!

如果你還不熟悉 switch 語句,使用 if/else 是完全沒問題的。

步驟 3:從你的元件使用 reducer

最後,你需要將 tasksReducer連接到你的元件。從 React 匯入useReducer Hook:

然後你可以替換useState

useReducer,如下所示:

這個useReducerHook 類似於useState——你必須傳遞一個初始狀態給它,它會返回一個有狀態的值和一個設定狀態的方法(在此例中為 dispatch 函數)。但它有些不同。

這個useReducerHook 接受兩個參數:

  1. 一個 reducer 函數
  2. 一個初始狀態

它返回:

  1. 一個有狀態的值
  2. 一個 dispatch 函數(用於將使用者操作「分派」給 reducer)

現在它已完全連接好了!這裡,reducer 宣告在元件檔案的底部:

如果你願意,甚至可以將 reducer 移到另一個檔案中:

當你像這樣分離關注點時,元件邏輯會更容易閱讀。現在事件處理器僅透過分派動作來指定發生了什麼,而 reducer 函數則決定狀態如何更新以回應它們。

比較 useStateuseReducer

Reducer 並非沒有缺點!以下是幾種比較它們的方式:

  • 程式碼大小:通常,使用 useState你一開始需要寫的程式碼較少。使用useReducer,你必須同時撰寫一個 reducer 函數以及dispatch 動作。然而,如果許多事件處理函數以類似的方式修改狀態,useReducer 可以幫助減少程式碼。
  • 可讀性:useState在狀態更新簡單時非常易於閱讀。當它們變得更加複雜時,它們可能會使你的元件程式碼膨脹並難以瀏覽。在這種情況下,useReducer讓你可以清晰地將更新邏輯的方式與事件處理函數的發生了什麼 分開。
  • 除錯:當你在使用 useState時遇到錯誤,可能很難判斷狀態是在哪裡 被錯誤地設定,以及 為什麼。使用useReducer,你可以在你的 reducer 中加入 console log 來查看每次狀態更新,以及它為什麼 發生(由於哪個 action)。如果每個action 都是正確的,你就會知道錯誤在 reducer 邏輯本身。然而,你必須比使用 useState時逐步檢查更多的程式碼。
  • 測試:Reducer 是一個不依賴於你的元件的純函數。這意味著你可以將其匯出並單獨隔離測試。雖然通常最好在更真實的環境中測試元件,但對於複雜的狀態更新邏輯,斷言你的 reducer 針對特定的初始狀態和動作返回特定的狀態可能很有用。
  • 個人偏好:有些人喜歡 reducers,有些人不喜歡。這沒關係。這是個人偏好的問題。你總是可以將useStateuseReducer來回轉換:它們是等效的!

我們建議,如果你經常因為某個元件中的狀態更新錯誤而遇到錯誤,並希望為其程式碼引入更多結構,則使用 reducer。你不必對所有事情都使用 reducers:可以隨意混合搭配!你甚至可以在同一個元件中同時使用useStateuseReducer

撰寫良好的 reducers

撰寫 reducers 時請記住這兩個技巧:

  • Reducer 必須是純粹的。 類似於 狀態更新函數,reducer 會在渲染期間執行!(動作會被排隊直到下一次渲染。)這意味著 reducer必須是純粹的——相同的輸入總是產生相同的輸出。它們不應該發送請求、安排計時器或執行任何副作用(影響元件外部事物的操作)。它們應該在不突變的情況下更新物件陣列
  • 每個動作描述一個單一的用戶互動,即使這會導致資料中的多個變更。例如,如果用戶在一個由 reducer 管理、包含五個欄位的表單上按下「重設」,那麼分派一個reset_form 動作比五個獨立的 set_field動作更合理。如果你在 reducer 中記錄每個動作,該記錄應該足夠清晰,讓你能夠重建互動或回應發生的順序。這有助於除錯!

使用 Immer 編寫簡潔的 Reducer

就像在常規狀態中更新物件陣列一樣,你可以使用 Immer 函式庫讓 reducer 更加簡潔。在這裡,useImmerReducer 允許你使用 pusharr[i] =賦值來突變狀態:

Reducer 必須是純粹的,因此它們不應該突變狀態。但 Immer 為你提供了一個特殊的draft 物件,可以安全地突變。在底層,Immer 會根據你對 draft所做的更改來建立狀態的副本。這就是為什麼由useImmerReducer管理的 reducer 可以突變其第一個參數,並且不需要返回狀態。

總結

  • 要從 useState轉換到useReducer
    1. 從事件處理函數分派動作。
    2. 編寫一個 reducer 函數,該函數針對給定的狀態和動作返回下一個狀態。
    3. useState替換為useReducer
  • Reducer 需要你編寫更多一點程式碼,但它們有助於除錯和測試。
  • Reducer 必須是純粹的。
  • 每個動作描述一個單一的用戶互動。
  • 如果你想以突變風格編寫 reducer,請使用 Immer。

Try out some challenges

Challenge 1 of 4:Dispatch actions from event handlers #

Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.