### 5-1 React 中的副作用處理:effect 初探
### 5-2 useEffect 其實不是 function component 的生命週期 API
### 5-3 維護資料流的聯動: 不要欺騙 hooks 的 dependencies
#### 《React 思維進化》
2025/5/13 導讀人:Tay
---
## 回顧:
- useEffect 不是 functional component 生命週期
- 隨著 re-render 不斷重新執行,每次都會產生對應結構的 React element
- 每次 render 的 state / props 都是獨立的 snapshot (快照),數值永遠保持不變
- render 內函式以 closure 記住,props/state 都是 immutable,所以函式執行結果固定且可預期
- Hooks 僅可以在 component function 中被呼叫
---
## 5-1 React 中的副作用處理:effect 初探
---
## Effect (副作用)
當函式除了回傳數值外,還會依賴或是影響函式外部的狀態或是會和外部互動時,就稱這個函式有副作用
EX:讀寫資料、API請求...etc
---
## 副作用的缺點
---
1. 可預測性降低:可能會被函式外部影響
2. 測試困難:可能涉及外部資源,需要模擬或隔離這些因素
3. 高耦合度:函式依賴或影響外部,這樣讓重構程式會變得困難
4. 難以閱讀&理解:因為互相依賴,所以單看函式不見得可以理解意義
5. 優化限制:可能限制編譯器優化的能力(GPT:有副作用的函式不能保證每次輸入相同都會輸出相同,所以 React 或 JS 引擎不敢亂優化)
---
## React component function 中的副作用
---
### component function 就是 function
### 所以副作用的缺點也是共通的
---
1. 函式多次執行所疊加的副作用影響難以預測
```javascript!
let globalVariable = 0
function calculateDouble(number){
// 每次函式執行時,外部環境的 globalVariable 都會 +1
globalVariable += 1
// 每次這個函式執行,就會發起新的網路請求
fetch(/* ... */).then(res => {
/* ... */
})
// 每次這個函式執行,就會修改一次 DOM element
document.getElementById('app').style.color = 'red'
}
```
---
2. 副作用可能會拖慢甚至阻塞函式本身的計算流程
```javascript!
function calculateDouble(number) {
// 修改 DOM element 的動作會需要和瀏覽器環境互動
// 在修改完成之前,程式碼就無法往下執行
document.getElementById('app').style.color = 'red'
return number * 2
}
```
---
## 為什麼我們需要 useEffect 來處理副作用
---
從剛剛的例子可以看到
不應在 component function 中直接進行有副作用的處理,可能會因為 re-render 而不斷疊加副作用
只用 state / props 的 component 當然不用擔心,但實務上一定需要打api、存取外部狀態 … etc
---
### useEffect 管理2個重要問題
---
**1. 清除或逆轉副作用造成的影響**
有副作用的 component,在 re-render 過程中會疊加副作用。
需要重複執行副作用時,應將前一次的副作用影響消除或是逆轉
---
**2. 在 render 過程中隔離副作用的執行時機:**
副作用的處理隔離到每次 render 流程完成後才執行
---
## 初次見面,useEffect()
---
### useEffect 處理副作用三大步驟
1. 定義 effect function 處理副作用
2. 加上 cleanup function 清理副作用(optional)
3. 指定 dependencies,以跳過不必要的副作用處理
---
### useEffect 語法
```javascript!
useEffect(effectFunction, dependencies)
// effectFunction:處理副作用 & 回傳cleanup函式:清除副作用(可選)
// dependencies:決定 effect 函式的觸發條件清單
```
---
### effectFunction
- 放置副作用的處理邏輯
- 若副作用是需要被清除的,可回傳清理副作用流程的 cleanup 函式
---
### dependencies
- 可選填陣列參數,應包含 effect function 中所有依賴到的 component 資料項目
- 沒有 dependencies 的話,effect function 會在每次 render 之後都被執行一次
- 有 dependencies 的話,在 re-render 時會以 Object.is 比較所有依賴項的值和前一次 render 的版本是否相同,都相同的話跳過本次 effect function
---
### 使用方式
```javascript!
useEffect(
() => {
OrderAPI.subscribeStatus(props.id, handleChange)
// cleanup 函式
// 每次 effect 函式要被執行前,執行上次render 版本的 cleanup 取消上次的訂閱
// 之後才進行本次的 render 的副作用
return () => {
OrderAPI.unsubscribeStatus(props.id, handleChange)
}
},
// 設定 dependencies,dependencies有改變時才會重新執行 effect function
[prop.id]
)
```
---
#### 一次 render 中 effect 函式和 cleanup 函式執行順序
---
1. 以本次 render 的 props 和 states 產生對應的 React element
2. 瀏覽器完成實際畫面 DOM 的繪製或操作
3. 執行前一次 render 版本的 cleanup 函式,以清理前一次 render 的 effect 函式所造成的影響
4. 執行本次 render 版本的 effect 函式。
---
## 每次 render 都有自己版本的 effect / cleanup 函式
---
### 範例1
```javascript!
import { useEffect } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
useEffect(
// 每次 re-render 都會是新的 effect function
// 所以 count 的數值也會當下的 snapshot
() => {
document.title = `You clicked ${count} times`
}
)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={ () => setCount(count+1) }>
Click me
</button>
</div>
)
}
```
---
### 範例2
```javascript!
// step1:props => { id: 1 }
// step2:props => { id: 2 }
// 每次 re-render 時,都有新的 props 快照
useEffect(
() => {
OrderAPI.subscribeStatus(props.id, handleStatusChange)
// id = 2 時,就會執行上次 id = 1 的 cleanup 函式
return () => {
OrderAPI.unsubscribeStatus(props.id,handleStatusChange)
}
}
)
```
---
## Q&A
---
Q: 什麼是副作用?為什麼我們需要透過 useEffect 在 React component function 中處理副作用?
---
A: 副作用是指函式除了回傳值外,還與外部互動或產生其他影響。
用來管理副作用,避免副作用的疊加,以及減少效能問題。
---
Q: useEffect 處理副作用的三大步驟?
---
A:
1. 定義 effect function 處理副作用
2. 加上 cleanup function 清理副作用(optional)
3. 指定 dependencies,以跳過不必要的副作用處理
---
Q: 『每次 render 都有其版本的 effect 與 cleanup 函式』是什麼意思?
---
A: 每次 component render 過程中產生的函式,都會透過 JavaScript 的 closure,捕捉並保留當次 render 的變數與狀態值,因此每一次 render 都有自己版本的函式和資料快照。
---
Q: 一次 render 中的 effect 函式會在什麼時間點被執行?
---
A:
1. 以本次 render 的 props 和 states 產生對應的 React element;
2. 瀏覽器完成實際畫面 DOM 的繪製或操作;
3. 執行前一次 render 版本的 cleanup 函式,以清理前一次 render 的 effect 函式所造成的影響;
4. 執行本次 render 版本的 effect 函式。
---
## 5-2 useEffect 其實不是 function component 的生命週期 API
---
## 宣告式 VS 指令式 程式設計
---
- **宣告式程式設計(Declarative Programming)**:告訴電腦「**要做什麼**」
- **指令式程式設計(Imperative Programming)**:告訴電腦「**怎麼做**」
---
useEffect 也是同樣原理,著重在副作用的處理,但不關心是在 mount 或是 update 時執行
---
## 為何以 useEffect 資料流同步化取代生命週期 API
---
class component 原有的生命週期 API 設計:
```javascript!
componentDidMount(){
OrderAPI.subscribeStatus(
this.props.id,
this.handleStatusChange
);
}
componentWillUnmount(){
OrderAPI.unsubscribeStatus(
this.props.id,
this.handleStatusChange
)
}
```
this.prod.id 更新 re-render 時,並不會以新 id 重新訂閱,以及取消舊id的訂閱
會導致 memory leak
---
```javascript!
componentDidMount(){
OrderAPI.subscribeStatus(
this.props.id,
this.handleStatusChange
);
}
// 取消訂閱前一次 render 的 prevProps.id 所對應的訂單狀態
componentDidUpdate(prevProps){
OrderAPI.unsubscribeStatus(
this.prevProps.id,
this.handleStatusChange
);
// 訂閱本次 render 的 props.id 所對應的訂單狀態
OrderAPI.subscribeStatus(
this.props.id,
this.handleStatusChange
)
}
componentWillUnmount(){
OrderAPI.unsubscribeStatus(
this.props.id,
this.handleStatusChange
)
}
```
忘記處理 componentDidUpate 是 class component 中常見的 bug
---
同邏輯在 function component 中,只需一個 useEffect 就可以:
```javascript!
useEffect(() => {
OrderAPI.subscribeStatus(props.id, handleChange);
return () => {
OrderAPI.unsubscribeStatus(props.id, handlechange)
}
})
```
---
## 認識Dependencies
---
1. 沒有Dependencies,每次 re-render 都會執行 effect 函式
```javascript!
import { useEffect } from 'react';
export default function App(props){
const [count,setCount] = useState(0);
// 每次re-render都會執行effect函式
useEffect(
() => {
document.title = `Hello ${props.name}`
}
)
return (
<button onClick={() => setCount(count+1)}>
Click me
</button>
)
}
```
---
2. 有設定 Dependencies,re-render時,Dependencies各項數值任一有變動才會執行 effect 函式
```javascript!
import { useEffect } from 'react';
export default function App(props){
const [count,setCount] = useState(0);
// re-render時,檢查 prod.name 是否和之前相同
// 不同的話才執行 effect 函式
useEffect(
() => {
document.title = `Hello ${props.name}`
},
[props.name]
)
return (
<button onClick={() => setCount(count+1)}>
Click me
</button>
)
}
```
---
3. Dependencies 為空陣列,只執行第一次
```javascript!
import { useEffect } from 'react';
export default function App(props){
const [count,setCount] = useState(0);
// 沒有依賴項,所以除了第一次以外
// 之後每次 react 都會跳過 effect 函式
useEffect(
() => {
document.title = `Hello ${props.name}`
},
[]
)
return (
<button onClick={() => setCount(count+1)}>
Click me
</button>
)
}
```
---
應該只在 effect 真的沒有依賴項的時候再用空陣列
不要為了只想執行一次而用空陣列
控制在第一次執行的話,違反 useEffect 設計思維
---
## Q&A
---
Q:useEffect 是 function component 的生命週期 API 嗎?為什麼?
---
A:不是生命週期API,是宣告式的同步化。useEffect用途是「將原始資料同步到畫面以外的副作用處理上」
---
Q:為什麼 React 要以 useEffect 的資料流同步化來取代生命週期 API?
---
A:舊有生命週期,需要考慮什麼時候做什麼動作,很容易會有遺漏。使用 useEffect 可以降低這種情況
---
Q: useEffect 的 dependencies 機制的設計目的與用途是什麼?
---
A:是效能優化而非邏輯控制,讓React知道此副作用是依賴於哪些資料,便在render時比較依賴項的異同,決定是否執行副作用
---
Q:可以用 depencies 來模擬 function component 生命週期 API 的效果嗎?
---
A:不行,需要確實填寫副作用依賴的資料。不誠實的 dependencies 會讓程式碼有危害。
---
## 5-3 維護資料流的連動:不要欺騙 hooks 的 dependencies
---
## 欺騙 dependencies 會造成的問題
---
[Demo](https://codesandbox.io/p/sandbox/qr-code-5-3-1-forked-d3rl64)
```javascript!
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(
() => {
const id = setInterval(
() => {
setCount(count + 1);
},
1000
);
return () => clearInterval(id);
},
[]
);
return <h1>{count}</h1>;
}
```
---
## 函式型別的依賴
---
### 函式型別依賴的常見錯誤
---
不誠實填寫 dependencies

---
誠實填寫 dependencies,但效能優化失敗

---
### 解決方式
---
1. 函式定義移到 effect 函式中

---
2. 把 component 資料流無關流程抽到外部

---
3. 把 useEffect 依賴的函式以 useCallback 包裹
```javascript!
const memoizedCallback = useCallback(fn, [dependencies]);
// fn:要記憶的函式
// [dependencies]:當這些依賴值沒變時,fn 不會重新定義
// (和 useEffect 類似,只是這裡必填)
```
---

---
### 以 linter 來輔助填寫 dependencies
[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)
[VS Code plugin - ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
---
## Effect dependencies 常見的錯誤用法
---
[Demo](https://codesandbox.io/p/sandbox/74tldf)
你以為 dependencies 設定為 [] 就只會執行一次嗎?
在嚴格模式下的開發環境會執行兩次
---
### 常見誤用1:在 function component 中模擬 ComponentDidMount
---
只想執行一次的話,應該自己寫邏輯 ([Demo](https://codesandbox.io/p/sandbox/qwy3jl))

---
### 常見誤用2:以 dependencies 來判斷副作用處理在特定資料更新時的執行時機
---
若是有需求是 todos 被更新時,需要執行 count + 1
---
錯誤做法:
硬把 todos 寫進依賴 dependencies

---
正確做法:
應該把 todo 的比較也寫進 effect 函式
這樣就可以正確依賴 todos

---
## Q&A
---
Q:如果欺騙 useEffect 的 dependencies 會造成什麼問題?
---
A:會導致明明依賴到的資料有更新時,卻跳過應該連動執行的同步化動作
---
Q:我們如何讓函式參與到 component 的資料流連動當中?
---
A:使用 useCallback 可以讓函數參與資料流當中。正確填寫 depencies 讓函式依賴資料有更新才會跟著改變。
---
Q:希望控制副作用處理邏輯在特定時機或條件下才執行時,應該如何做到?
---
A:dependencies 應該正確填寫依賴的資料,其餘的商業邏輯應自已撰寫條件式
---
# The End
---
{"title":"回顧:","description":"Multi-columns effect cannot be previewed.Apply the template to display the multi-columns effect.","contributors":"[{\"id\":\"21051f81-ff78-4f9c-81ae-8102c6e35bcc\",\"add\":11070,\"del\":824}]"}