將事件與 Effect 分離
事件處理函式僅在您再次執行相同互動時才會重新執行。與事件處理函式不同,如果 Effect 讀取的某些值(例如 prop 或狀態變數)與上次渲染時的值不同,Effect 會重新同步。有時,您也會希望混合這兩種行為:一個 Effect 會因應某些值而重新執行,但不會因應其他值。本頁將教您如何做到這一點。
您將學習
- 如何在事件處理函式與 Effect 之間做出選擇
- 為何 Effect 是響應式的,而事件處理函式不是
- 當您希望 Effect 的部分程式碼不具響應性時該怎麼做
- 什麼是 Effect Event,以及如何從您的 Effect 中提取它們
- 如何使用 Effect Event 從 Effect 讀取最新的 props 和狀態
在事件處理函式與 Effect 之間做出選擇
讓我們回顧一下事件處理函式與 Effect 之間的區別。
假設您正在實作一個聊天室元件。您的要求如下:
- 您的元件應自動連線到選定的聊天室。
- 當您點擊「傳送」按鈕時,應將訊息傳送到聊天室。
假設您已經為它們實作了程式碼,但不確定該放在哪裡。您應該使用事件處理函式還是 Effect?每次您需要回答這個問題時,請考慮程式碼為何需要執行。
事件處理函式因應特定互動而執行
從使用者的角度來看,傳送訊息應該發生是因為特定的「傳送」按鈕被點擊了。如果您在其他時間或出於其他原因傳送他們的訊息,使用者會相當不悅。這就是為什麼傳送訊息應該是一個事件處理函式。事件處理函式讓您處理特定的互動:
使用事件處理函式,您可以確保sendMessage(message)僅在使用者按下按鈕時才會執行。
Effect 在需要同步時執行
回想一下,您還需要讓元件保持連線到聊天室。那段程式碼應該放在哪裡?
執行這段程式碼的原因並非某個特定的互動。使用者為何或如何導航到聊天室畫面並不重要。既然他們正在查看它並可能與之互動,元件就需要保持連線到選定的聊天伺服器。即使聊天室元件是您應用程式的初始畫面,且使用者尚未執行任何互動,您仍然需要連線。這就是為什麼它是一個 Effect:
使用這段程式碼,您可以確保始終有一個到當前選定聊天伺服器的活動連線,無論使用者執行了哪些特定互動。無論使用者只是打開了您的應用程式、選擇了不同的房間,還是導航到另一個畫面並返回,您的 Effect 都能確保元件將保持與當前選定房間的同步,並將在必要時重新連線。
響應式值與響應式邏輯
直觀地說,您可以說事件處理函式總是「手動」觸發的,例如透過點擊按鈕。另一方面,Effect 是「自動」的:它們會執行並重新執行,以保持同步所需的頻率。
有一種更精確的方式來思考這個問題。
Props、狀態以及在元件主體內宣告的變數被稱為響應式值。在此範例中,serverUrl不是響應式值,但
像這樣的響應式值可能會因為重新渲染而改變。例如,使用者可能會編輯message或在下拉選單中選擇不同的roomId。事件處理函式和 Effect 對變化的回應方式不同:
- 事件處理函式內的邏輯是非響應式的。除非使用者再次執行相同的互動(例如點擊),否則它不會再次執行。事件處理函式可以讀取響應式值,但不會「響應」它們的變化。
- Effect 內的邏輯是響應式的。如果你的 Effect 讀取了一個響應式值,你必須將其指定為依賴項。然後,如果重新渲染導致該值發生變化,React 將使用新值重新執行你的 Effect 邏輯。
讓我們回顧一下前面的例子來說明這種差異。
事件處理函式內的邏輯是非響應式的
看看這行程式碼。這個邏輯應該是響應式的還是不響應式的?
從使用者的角度來看,對 message的更改並不意味著他們想要發送訊息。 這只表示使用者正在輸入。換句話說,發送訊息的邏輯不應該是響應式的。它不應該僅僅因為 響應式值發生了變化就再次執行。這就是為什麼它屬於事件處理函式:
事件處理函式不是響應式的,所以sendMessage(message)只會在使用者點擊傳送按鈕時執行。
Effect 內的邏輯是響應式的
現在讓我們回到這幾行程式碼:
從使用者的角度來看,對 roomId的更改確實意味著他們想要連接到不同的房間。 換句話說,連接到房間的邏輯應該是響應式的。你 希望 這幾行程式碼能夠「跟上」響應式值的變化,並且在該值不同時再次執行。這就是為什麼它屬於 Effect:
Effect 是響應式的,所以createConnection(serverUrl, roomId) 和 connection.connect() 會針對每個不同的 roomId值執行。你的 Effect 使聊天連線與當前選定的房間保持同步。
從 Effect 中提取非響應式邏輯
當你想要混合響應式邏輯和非響應式邏輯時,事情會變得更加棘手。
例如,假設你想在使用者連接到聊天時顯示通知。你從 props 中讀取當前主題(深色或淺色),以便以正確的顏色顯示通知:
然而,theme是一個響應式值(它可能因重新渲染而改變),並且Effect 讀取的每個響應式值都必須宣告為其依賴項。 現在你必須將 theme指定為你的 Effect 的依賴項:
試試這個例子,看看你是否能發現這個使用者體驗的問題:
當 roomId改變時,聊天室會如預期般重新連接。但由於theme也是一個依賴項,每次你在深色和淺色主題之間切換時,聊天室也會重新連接。這可不太好!
換句話說,你並不希望這行程式碼具有響應性,即使它位於一個 Effect(本身是響應式的)內部:
你需要一種方法,將這段非響應式的邏輯與其周圍的響應式 Effect 分離開來。
宣告 Effect Event
使用一個名為 useEffectEvent的特殊 Hook,將這段非響應式邏輯從你的 Effect 中提取出來:
在這裡,onConnected被稱為一個Effect Event(Effect 事件)。它是你 Effect 邏輯的一部分,但其行為更像一個事件處理器。其內部的邏輯不是響應式的,並且它總是能「看到」你的 props 和 state 的最新值。
現在你可以從你的 Effect 內部呼叫onConnected這個 Effect Event:
這解決了問題。請注意,你必須從你的 Effect 的依賴項列表中移除theme,因為它不再在 Effect 中被使用。你也不需要 將onConnected加入其中,因為Effect Events 不是響應式的,必須從依賴項中省略。
驗證新的行為是否符合你的預期:
你可以將 Effect Events 視為與事件處理器非常相似。主要區別在於,事件處理器是響應使用者互動而執行,而 Effect Events 則是由你從 Effects 中觸發。Effect Events 讓你能夠「打破」 Effects 的響應性與不應是響應式的程式碼之間的鏈結。
使用 Effect Events 讀取最新的 props 和 state
Effect Events 讓你能夠修正許多你可能會想要抑制依賴檢查器(dependency linter)的模式。
例如,假設你有一個用於記錄頁面訪問的 Effect:
之後,您為您的網站新增了多個路由。現在您的Page元件接收一個帶有當前路徑的url prop。您想將 url 作為 logVisit呼叫的一部分傳遞,但依賴檢查工具抱怨道:
思考一下您希望程式碼做什麼。您希望為不同的 URL 記錄單獨的訪問,因為每個 URL 代表一個不同的頁面。換句話說,這個logVisit呼叫應該 對 url具有響應性。這就是為什麼,在這種情況下,遵循依賴檢查工具並將url新增為依賴項是合理的:
現在假設您想在每次頁面訪問時包含購物車中的商品數量:
您在 Effect 內部使用了numberOfItems,因此檢查工具要求您將其新增為依賴項。然而,您 不 希望 logVisit 呼叫對 numberOfItems具有響應性。如果使用者將某物放入購物車,並且numberOfItems發生變化,這並不意味著使用者再次訪問了該頁面。換句話說,訪問頁面在某種意義上是一個「事件」。它發生在一個精確的時間點。
將程式碼分成兩部分:
在這裡,onVisit是一個 Effect Event。其內部的程式碼不具有響應性。這就是為什麼您可以使用numberOfItems(或任何其他響應式值!)而不用擔心它會導致周圍的程式碼在變化時重新執行。
另一方面,Effect 本身仍然是響應式的。Effect 內部的程式碼使用了urlprop,因此 Effect 將在每次使用不同url重新渲染後重新執行。這反過來又會呼叫onVisitEffect Event。
因此,您將為 url 的每次變化呼叫 logVisit,並且總是讀取最新的numberOfItems。然而,如果numberOfItems自行變化,這將不會導致任何程式碼重新執行。
注意
您可能想知道是否可以不帶參數呼叫onVisit(),並在其內部讀取 url:
這會起作用,但最好將這個 url明確傳遞給 Effect Event。透過將 url作為參數傳遞給您的 Effect Event,您是在表示從使用者的角度來看,訪問具有不同url 的頁面構成了一個獨立的「事件」。 visitedUrl是發生的「事件」的一部分:
由於您的 Effect Event 明確地「要求」visitedUrl,現在您不可能意外地從 Effect 的依賴項中移除url。如果您移除 url 依賴項(導致不同的頁面訪問被計為一次),檢查工具會警告您。您希望 onVisit 對 url具有響應性,因此與其在內部讀取
Effect Events 的限制
Effect Events 在使用方式上受到很大限制:
- 只能在 Effect 內部呼叫它們。
- 絕不要將它們傳遞給其他元件或 Hooks。
例如,不要像這樣宣告並傳遞一個 Effect Event:
相反地,應該總是將 Effect Events 直接宣告在使用它們的 Effect 旁邊:
Effect Events 是您 Effect 程式碼中非響應式的「片段」。它們應該緊鄰使用它們的 Effect。
總結
- 事件處理器在回應特定互動時執行。
- Effect 在需要同步時執行。
- 事件處理器內的邏輯是非響應式的。
- Effect 內的邏輯是響應式的。
- 您可以將 Effect 中的非響應式邏輯移到 Effect Events 中。
- 只能從 Effect 內部呼叫 Effect Events。
- 不要將 Effect Events 傳遞給其他元件或 Hooks。
Try out some challenges
Challenge 1 of 4:Fix a variable that doesn’t update #
This Timer component keeps a count state variable which increases every second. The value by which it’s increasing is stored in the increment state variable. You can control the increment variable with the plus and minus buttons.
However, no matter how many times you click the plus button, the counter is still incremented by one every second. What’s wrong with this code? Why is increment always equal to 1 inside the Effect’s code? Find the mistake and fix it.
