v19.2Latest

移除 Effect 依賴項

當你編寫一個 Effect 時,linter 會驗證你是否將 Effect 讀取的每個響應式值(例如 props 和 state)都包含在你的 Effect 依賴項列表中。這確保了你的 Effect 與元件的最新 props 和 state 保持同步。不必要的依賴項可能會導致你的 Effect 執行過於頻繁,甚至產生無限迴圈。請遵循本指南來審查並從你的 Effects 中移除不必要的依賴項。

您將學習
  • 如何修復無限的 Effect 依賴項迴圈
  • 當你想移除一個依賴項時該怎麼做
  • 如何從你的 Effect 中讀取值而不對其產生「響應」
  • 如何以及為何要避免物件和函數依賴項
  • 為何抑制依賴項 linter 是危險的,以及應該怎麼做

依賴項應與程式碼相符

當你編寫一個 Effect 時,你首先需要指定如何啟動和停止你想要 Effect 執行的任何操作:

然後,如果你將 Effect 依賴項留空([]),linter 會建議正確的依賴項:

根據 linter 的提示填入它們:

Effects 會「響應」響應式值。由於roomId是一個響應式值(它可能因重新渲染而改變),linter 會驗證你已將其指定為依賴項。如果roomId接收到不同的值,React 將重新同步你的 Effect。這確保了聊天室保持連接到所選的房間,並對下拉選單產生「響應」:

要移除一個依賴項,請證明它不是依賴項

請注意,您不能「選擇」您的 Effect 的依賴項。您的 Effect 程式碼中使用的每個響應式值都必須在您的依賴項列表中宣告。依賴項列表是由周圍的程式碼決定的:

響應式值包括 props 以及直接在您的元件內部宣告的所有變數和函數。由於roomId是一個響應式值,您不能將其從依賴項列表中移除。linter 不會允許這樣做:

而 linter 是對的!由於roomId可能會隨時間改變,這將在您的程式碼中引入一個錯誤。

要移除一個依賴項,請向 linter「證明」它不需要成為一個依賴項。例如,您可以將roomId移出您的元件,以證明它不是響應式的,並且不會在重新渲染時改變:

現在roomId不是一個響應式值(並且不會在重新渲染時改變),它就不需要成為一個依賴項:

這就是為什麼你現在可以指定一個空([])依賴陣列。你的 Effect確實不再依賴任何響應式值,因此當元件的任何 props 或狀態改變時,它也確實不需要重新執行。

要改變依賴項,請先改變程式碼

你可能已經注意到工作流程中的一個模式:

  1. 首先,你改變 Effect 的程式碼或改變響應式值的宣告方式。
  2. 然後,你遵循 linter 的提示,調整依賴項以匹配你已改變的程式碼。
  3. 如果你對依賴項清單不滿意,就回到第一步(再次改變程式碼)。

最後一部分很重要。如果你想改變依賴項,請先改變周圍的程式碼。你可以將依賴項清單視為Effect 程式碼所使用的所有響應式值的清單。你並不是選擇要將什麼放入該清單。該清單描述了你的程式碼。要改變依賴項清單,請改變程式碼。

這感覺可能像是在解方程式。你可能從一個目標開始(例如,移除一個依賴項),然後你需要「找到」符合該目標的程式碼。並非每個人都覺得解方程式有趣,編寫 Effect 也是如此!幸運的是,下面有一份常見的解決方案清單可供你嘗試。

陷阱

如果你有一個現有的程式碼庫,可能會有一些 Effect 像這樣抑制 linter:

當依賴項與程式碼不匹配時,引入錯誤的風險非常高。透過抑制 linter,你是在向 React「謊報」你的 Effect 所依賴的值。

請改用下面的技巧。

移除不必要的依賴項

每次你調整 Effect 的依賴項以反映程式碼時,請查看依賴項清單。當這些依賴項中的任何一個發生變化時,Effect 重新執行是否合理?有時,答案是「否」:

  • 您可能希望在不同條件下重新執行 Effect 的不同部分
  • 您可能只想讀取某個依賴項的最新值,而不是對其變化做出「反應」。
  • 某個依賴項可能因為是物件或函數而無意中過於頻繁地變化。

要找到正確的解決方案,您需要回答幾個關於您的 Effect 的問題。讓我們逐一探討。

這段程式碼應該移到事件處理函數中嗎?

您首先應該考慮的是這段程式碼是否應該是一個 Effect。

想像一個表單。提交時,您將狀態變數submitted 設為 true。您需要發送一個 POST 請求並顯示通知。您將此邏輯放在一個對 submittedtrue做出「反應」的 Effect 中:

之後,您想根據當前主題樣式化通知訊息,因此您讀取當前主題。由於 theme宣告在元件主體中,它是一個響應式值,因此您將其添加為依賴項:

這樣做,您引入了一個錯誤。想像您先提交表單,然後在深色和淺色主題之間切換。theme會改變,Effect 會重新執行,因此它會再次顯示相同的通知!

這裡的問題在於,這從一開始就不應該是一個 Effect。 您希望在提交表單這一特定互動時發送此 POST 請求並顯示通知。要在特定互動時執行某些程式碼,請將該邏輯直接放入相應的事件處理函數中:

現在程式碼位於事件處理函數中,它不是響應式的——因此只會在用戶提交表單時執行。閱讀更多關於在事件處理函數和 Effect 之間做出選擇以及如何刪除不必要的 Effect

您的 Effect 是否在做幾件不相關的事情?

您應該問自己的下一個問題是您的 Effect 是否在做幾件不相關的事情。

假設您正在建立一個運輸表單,用戶需要選擇他們的城市和區域。您根據所選的cities 從伺服器獲取country列表,以在下拉選單中顯示它們:

這是一個在 Effect 中獲取資料的好例子。您正在根據cities 屬性將country狀態與網路同步。您不能在事件處理函數中執行此操作,因為您需要在ShippingForm 顯示時以及每當country變化時(無論是哪種互動引起的)立即獲取。

現在假設您要為城市區域添加第二個選擇框,它應該為當前選定的areas 獲取city。您可能會從在同一個 Effect 中添加第二個fetch 呼叫來獲取區域列表開始:

然而,由於 Effect 現在使用了city 狀態變數,你不得不將 city加入到依賴項列表中。這反過來又引入了一個問題:當用戶選擇不同的城市時,Effect 將重新執行並呼叫fetchCities(country)。結果,你將不必要地多次重新獲取城市列表。

這段程式碼的問題在於你正在同步兩個不相關的不同事物:

  1. 你希望根據 country prop 將 cities狀態同步到網路。
  2. 你希望根據 city 狀態將 areas狀態同步到網路。

將邏輯拆分為兩個 Effect,每個 Effect 都對其需要同步的 prop 做出反應:

現在第一個 Effect 僅在country改變時重新執行,而第二個 Effect 則在city改變時重新執行。你已按目的將它們分開:兩個不同的事物由兩個獨立的 Effect 同步。兩個獨立的 Effect 有兩個獨立的依賴項列表,因此它們不會無意中觸發彼此。

最終的程式碼比原始版本長,但拆分這些 Effect 仍然是正確的。每個 Effect 都應該代表一個獨立的同步過程。在這個例子中,刪除一個 Effect 並不會破壞另一個 Effect 的邏輯。這意味著它們同步的是不同的事物, 因此將它們分開是好的。如果你擔心重複,可以透過 將重複的邏輯提取到自訂 Hook 中 來改進這段程式碼。

你是否正在讀取某些狀態來計算下一個狀態?

這個 Effect 在每次新訊息到達時,用一個新建立的陣列來更新messages 狀態變數:

它使用 messages 變數來 建立一個新陣列,該陣列以所有現有訊息開頭並在末尾新增新訊息。然而,由於messages是一個被 Effect 讀取的反應值,它必須是一個依賴項:

而將 messages設為依賴項會引入一個問題。

每次你收到訊息時,setMessages()會導致元件重新渲染,並使用一個包含已接收訊息的新messages 陣列。然而,由於這個 Effect 現在依賴於 messages,這將 同時重新同步 Effect。因此,每條新訊息都會使聊天重新連接。用戶不會喜歡這樣!

要解決這個問題,不要在 Effect 內部讀取messages。相反,傳遞一個 更新函數setMessages

請注意您的 Effect 現在完全沒有讀取messages 變數。您只需要傳遞一個更新函數,例如msgs => [...msgs, receivedMessage]。React 會將您的更新函數放入佇列,並在下一次渲染時提供msgs 參數給它。這就是為什麼 Effect 本身不再需要依賴 messages。這個修正的結果是,接收聊天訊息將不再導致聊天重新連線。

您是否想讀取一個值而不「回應」其變化?

假設您想在用戶收到新訊息時播放聲音,除非 isMutedtrue

由於您的 Effect 現在在其程式碼中使用了isMuted,您必須將其加入依賴項:

問題在於每當 isMuted變化時(例如,當用戶按下「靜音」切換按鈕),Effect 將重新同步,並重新連線到聊天室。這不是期望的使用者體驗!(在這個例子中,即使停用 linter 也無法解決問題——如果您這樣做,isMuted 將會「卡住」其舊值。)

為了解決這個問題,您需要將不應具有反應性的邏輯從 Effect 中提取出來。您不希望這個 Effect「回應」isMuted的變化。將這段非反應性的邏輯移至 Effect Event 中:

Effect Events 讓您可以將 Effect 拆分為反應性部分(應該「回應」像roomId這樣的反應性值及其變化)和非反應性部分(僅讀取其最新值,例如onMessage 讀取 isMuted)。既然您在 Effect Event 內部讀取isMuted,它就不需要成為您 Effect 的依賴項。因此,當您切換「靜音」設定時,聊天室將不會重新連線,從而解決了原始問題!

包裝來自 props 的事件處理器

當您的元件接收一個事件處理器作為 prop 時,您可能會遇到類似的問題:

假設父元件在每次渲染時傳遞一個不同的onReceiveMessage 函數:

由於onReceiveMessage是一個依賴項,它會導致 Effect 在每次父元件重新渲染後重新同步。這將使其重新連線到聊天室。為了解決這個問題,請將呼叫包裝在 Effect Event 中:

Effect Events 不具有反應性,因此您不需要將它們指定為依賴項。因此,即使父元件傳遞一個每次重新渲染都不同的函數,聊天室也不會再重新連線。

分離反應性與非反應性程式碼

在這個例子中,您希望在每次 roomId變化時記錄一次訪問。您希望每次記錄都包含當前的notificationCount,但您不希望 notificationCount的變化觸發記錄事件。

解決方案仍然是將非反應性程式碼提取到 Effect Event 中:

你希望你的邏輯對 roomId具有響應性,所以你在 Effect 內部讀取roomId。然而,你不希望 notificationCount的變化導致記錄額外的訪問,所以你將notificationCount放在 Effect Event 內部讀取。深入了解如何使用 Effect Event 從 Effect 中讀取最新的 props 和 state。

是否有某些響應值意外地改變了?

有時,你確實希望你的 Effect 對某個值做出「響應」,但該值的變化頻率超出了你的預期——並且可能並不反映用戶視角下的任何實際變化。例如,假設你在元件主體中建立了一個options 物件,然後從你的 Effect 內部讀取該物件:

此物件宣告在元件主體中,因此它是一個響應值。當你在 Effect 內部讀取像這樣的響應值時,你需要將其宣告為依賴項。這確保了你的 Effect 會對其變化做出「響應」:

將其宣告為依賴項非常重要!例如,這確保了如果 roomId發生變化,你的 Effect 將使用新的options重新連接到聊天室。然而,上面的程式碼也存在一個問題。要看到這個問題,請嘗試在下面的沙箱中輸入內容,並觀察控制台中發生的事情:

在上面的沙箱中,輸入框僅更新 message 狀態變數。從用戶的角度來看,這不應該影響聊天連線。然而,每次你更新 message時,你的元件都會重新渲染。當你的元件重新渲染時,其內部的程式碼會從頭開始再次執行。

每次ChatRoom元件重新渲染時,都會從頭建立一個新的options物件。React 會發現這個options物件與上一次渲染時建立的options物件是不同的物件。這就是為什麼它會重新同步你的 Effect(該 Effect 依賴於options),並且在你輸入時聊天會重新連接。

這個問題只影響物件和函數。在 JavaScript 中,每個新建立的物件和函數都被認為與其他所有物件和函數是不同的。即使它們內部的內容可能相同,這也無關緊要!

物件和函數依賴項可能導致你的 Effect 比所需更頻繁地重新同步。

這就是為什麼,只要有可能,你應該盡量避免將物件和函數作為 Effect 的依賴項。相反,嘗試將它們移到元件外部、移到 Effect 內部,或是從中提取出原始值。

將靜態物件和函數移到元件外部

如果物件不依賴於任何 props 和 state,你可以將該物件移到元件外部:

這樣一來,你向 linter證明了它不是響應式的。它不會因為重新渲染而改變,因此不需要成為依賴項。現在重新渲染 ChatRoom不會導致你的 Effect 重新同步。

這對函數也適用:

由於createOptions是在元件外部宣告的,它不是一個響應式值。這就是為什麼它不需要被指定為 Effect 的依賴項,也永遠不會導致你的 Effect 重新同步。

將動態物件和函數移到 Effect 內部

如果你的物件依賴於某些可能因重新渲染而改變的響應式值,例如 roomIdprop,你就不能將其移到元件外部。然而,你可以將其創建過程移到 Effect 的程式碼內部

現在 options是在你的 Effect 內部宣告的,它不再是 Effect 的依賴項。相反,你的 Effect 使用的唯一響應式值是roomId。由於roomId 不是物件或函數,你可以確定它不會無意中變得不同。在 JavaScript 中,數字和字串是根據其內容進行比較的:

得益於此修正,如果你編輯輸入框,聊天室將不再重新連接:

然而,當您更改確實會重新連接,正如您所預期的。roomId 下拉選單時,它

這對函數也適用:

您可以編寫自己的函數來將邏輯片段分組在您的 Effect 內部。只要您也將它們宣告您的 Effect 內部,它們就不是響應式值,因此不需要成為您 Effect 的依賴項。

從物件讀取原始值

有時,您可能會從 props 接收到一個物件:

這裡的風險在於父元件會在渲染期間建立物件:

這會導致您的 Effect 在父元件每次重新渲染時都重新連接。要修復此問題,請在 Effect外部從物件讀取資訊,並避免物件和函數依賴項:

邏輯變得有點重複(您在 Effect 外部從物件讀取一些值,然後在 Effect 內部建立一個具有相同值的物件)。但這使得您的 Effect實際依賴於哪些資訊變得非常明確。如果父元件無意中重新建立了物件,聊天室將不會重新連接。然而,如果options.roomIdoptions.serverUrl確實不同,聊天室將會重新連接。

從函數計算原始值

同樣的方法也適用於函數。例如,假設父元件傳遞一個函數:

為了避免使其成為依賴項(並導致其在重新渲染時重新連接),請在 Effect 外部呼叫它。這會給您roomIdserverUrl這些不是物件的值,您可以在 Effect 內部讀取它們:

這僅適用於 函數,因為它們在渲染期間呼叫是安全的。如果您的函數是一個事件處理器,但您不希望其變更重新同步您的 Effect,請改用 Effect Event 將其包裝起來。

總結

  • 依賴項應始終與程式碼相符。
  • 當您對依賴項不滿意時,您需要編輯的是程式碼。
  • 抑制 linter 會導致非常令人困惑的錯誤,您應始終避免這樣做。
  • 要移除一個依賴項,您需要向 linter「證明」它是不必要的。
  • 如果某些程式碼應在特定互動後執行,請將該程式碼移至事件處理函數中。
  • 如果您的 Effect 的不同部分應因不同原因重新執行,請將其拆分為多個 Effect。
  • 如果您想基於先前的狀態更新某些狀態,請傳遞一個更新函數。
  • 如果您想讀取最新值而不對其產生「反應」,請從您的 Effect 中提取一個 Effect Event。
  • 在 JavaScript 中,如果物件和函數是在不同時間建立的,則它們被視為不同。
  • 盡量避免物件和函數依賴項。將它們移至元件外部或 Effect 內部。

Try out some challenges

Challenge 1 of 4:Fix a resetting interval #

This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created.