### 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 ![image alt](https://hackmd.io/_uploads/B1gnNdebxg.png=50x100) --- 誠實填寫 dependencies,但效能優化失敗 ![image](https://hackmd.io/_uploads/Bk_GHdebee.png) --- ### 解決方式 --- 1. 函式定義移到 effect 函式中 ![image](https://hackmd.io/_uploads/ryHN8uxZeg.png) --- 2. 把 component 資料流無關流程抽到外部 ![image](https://hackmd.io/_uploads/Sy3HwOlbll.png) --- 3. 把 useEffect 依賴的函式以 useCallback 包裹 ```javascript! const memoizedCallback = useCallback(fn, [dependencies]); // fn:要記憶的函式 // [dependencies]:當這些依賴值沒變時,fn 不會重新定義 // (和 useEffect 類似,只是這裡必填) ``` --- ![image](https://hackmd.io/_uploads/H1qzaOlbgg.png) --- ### 以 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)) ![image](https://hackmd.io/_uploads/r16KsKg-eg.png) --- ### 常見誤用2:以 dependencies 來判斷副作用處理在特定資料更新時的執行時機 --- 若是有需求是 todos 被更新時,需要執行 count + 1 --- 錯誤做法: 硬把 todos 寫進依賴 dependencies ![image](https://hackmd.io/_uploads/SJe_yclWxx.png) --- 正確做法: 應該把 todo 的比較也寫進 effect 函式 這樣就可以正確依賴 todos ![image](https://hackmd.io/_uploads/Syetxqxblg.png) --- ## 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}]"}
    64 views