1
2025-04-13
#React

告別錯誤觀念 —— useEffect 不是生命週期 API

#前言

如標題所說,這篇文章要打破大家的觀念,介紹 useEffect 真正的用途。
在《React 思維進化》系列文章已有提到這部分,這篇文章是用自己的想法再重新梳理、解釋一次。

#React 的設計精神

首先我們要先知道,React 是宣告式設計(Declarative Programming),什麼是宣告式?
簡單來說,就是描述「是什麼」,而不是「怎麼做」。
我們只要描述「畫面要長什麼樣子」,React 會幫我們處理好變動。

例如:在真實 DOM 的操作上,React 會依據 state, props 產生虛擬 DOM,當它們有變動時,就會重新渲染元件,產生新版本的虛擬 DOM 畫面,然後將它們做比較,找出相異之處,只更新相異部分的真實 DOM。

而在 useEffect,我們也應順著宣告式設計的精神去思考,描述「目標」,而不是手動指定「怎麼一步一步達成這個目標」。因此,當使用 useEffect 時,重點不是去追究它「何時執行」的細節。

#useEffect 的用途

所以 useEffect 的用途是什麼呢?

在 React 的官方文件是這麼說的:

useEffect is a React Hook that lets you synchronize a component with an external system.

中文直翻就是:「useEffect 是一個讓你將元件和外部系統同步化的 React Hook。」

同步化?什麼意思?

React 組件運作在自己的世界中,有自己的生命週期和狀態管理。然而,應用程序通常需要與外部系統互動,例如:

  1. 發 API 請求
  2. 手動操作 DOM
  3. 設置定時器
  4. 第三方庫

這些外部系統不了解 React 的狀態更新或重新渲染機制。而 useEffect 就像一個橋梁,讓我們能夠:
在 React 狀態變化時,更新外部系統狀態;並在外部系統變化時,更新 React 狀態,也就是文件說的「將元件和外部系統同步化」,或者再更簡單點說,就是「保持狀態資料同步」。

所以 useEffect 不是 functional component 的生命週期 API,而是用來保持狀態資料同步的。

#useEffect 能夠將副作用的處理「隔離」

副作用指的是:函式會依賴或影響函式外的某些狀態、或是與外部環境產生互動。
所以,前面所說的「同步 React 元件與外部系統」,其實就是副作用的具體情境。因此我們也可以說,useEffect 是用來處理副作用的。

我們在開發時,當然希望元件要是可預測的,給定相同的 props 和 state,應該渲染出相同的結果。
useEffect 就能夠處理那些「不純」的操作(副作用),例如前面所提到的發 API 請求、操作 DOM...等。

useEffect 的執行時機是在:虛擬 DOM 渲染完,真實 DOM 也掛載或更新後(當然會再依據 dependencies 決定是否執行)。
這個機制讓我們把這些副作用「隔離」起來處理,而不是混進元件的 render 裡面。

#隔離有什麼好處

1. 讓元件與外部系統同步化

由於 useEffect 執行的時機是在虛擬 DOM 渲染完、真實 DOM 更新後才執行,我們就可以確保目前得到的資料是最新的。
這個「時機」很重要,因為它確保副作用的執行能準確反映當下 React 的狀態,也才能有效地將資料「同步」到外部世界。

2. 保持元件渲染結果的一致性

我們先說明這些副作用處理可能會如何造成資料不一致:

我們知道,元件的渲染過程要是「純」的,也就是說,給定相同的 state 和 props,每次渲染都應該產生相同的 UI。
而如果某些操作會影響到 state 或依賴外部環境,這就有可能破壞這個一致性。例如:

  • 如果我們直接在 render 中發送 API 請求(也就是沒有使用 useEffect 來隔離持行時機),因為 API 回應的時間不確定,就會導致每次渲染的結果不同。
  • 直接操作 DOM,React 就無法追蹤這些變更,導致 UI 和實際 DOM 的狀態不同步。

把副作用放在 useEffect 中隔離處理,就可以確保元件渲染過程純粹,因為 useEffect 是在元件渲染完才執行。
因此:

  1. 可以避免在元件渲染過程中執行非同步操作,保持資料的一致性
  2. 能確保元件的 render 只依賴於 state 和 props,每次渲染都是一樣的結果

#useEffect 的 dependencies

以前,我們可能誤將 useEffect 視為生命週期 API,並嘗試用 dependencies 陣列來控制執行時機。

而現在我們知道,useEffect 的重點是同步化、處理副作用,因此理論上,重複它並不會破壞應用的正確性。如果元件和外部系統已經是同步的了,再次執行這些操作應該得到相同的結果,不會造成錯誤。

那 dependencies 是做什麼的呢?

⭐️ 它是一種效能優化手段。

若我們有正確使用 useEffect 的話,那麼重複它其實也不會怎樣,但我們當然不想要、不需要每次都執行它,所以才需要 dependencies,當 dependencies 陣列裡的值沒有改變時,就會跳過 effect 函式。
因此,dependencies 不是用來做邏輯控制的,而是一種「跳過某些不必要的執行」的效能優化。

#useEffect 當成生命週期 API 使用會怎麼樣

就目前來說,其實也不會出什麼大問題,畢竟很多人習慣這樣用。
但這並不是 useEffect 真正的設計目的,既然它是用來處理副作用的,若沒有正確使用它,可能會在一些細微、不容易察覺的地方引發問題,導致難以除錯的狀況。

在未來的 React 版本中,加入了 Reusable State 的概念。
Reusable State 是指:從畫面中移除元件後,仍能保留其 state 狀態,以便需要時重新 mount 後再次還原。
而當然,在每次 mount 時,元件就會再次執行副作用的處理,這意味著,在未來版本的 React 中,即使 dependencies 沒有變化,effect 函式仍有可能再次被執行。
因此,若我們把 useEffect 當成生命週期 API 使用,用「effect 函式會在某某時機執行」這樣的思考模式,就有可能會出錯。

所以為了確保元件支援上述的特性,useEffect 就必須滿足「無論被重複多少次也不會壞掉」的目標。

#總結

在開發時,要從原本「生命週期」改成「同步化」的思維方式,個人認為不太容易,會需要一些時間思考 useEffect 用新的方法該怎麼寫,不像以前「某 state 改變時就做某事」那麼直觀。
以前開發時,曾有過這樣的經驗:一個元件用了許多 useEffect,而 dependencies 中又填入了好幾個參數,這樣錯綜複雜的執行時機依賴造成了開發、除錯上很大的困難。
因此,我認為當程式碼越來越複雜時,「同步化」的寫法確實更可靠、也更利於開發,若有正確寫好 useEffect 的話,基本上不會出錯。

而使用新的 useEffect 寫法時,我有兩個建議:

  1. 暫時把 dependencies 拿掉,看看是否重複執行也不會壞掉
  2. useEffect 中用到什麼變數,就要填入 dependencies(也就是要對 dependencies 誠實,關於這部分,可以參考《React 思維進化》#5-3 維護資料的流動:不要欺騙 hooks 的 dependencies
main*
© 2024 All Rights Reserved. IRIS Studio