# React 思維進化 * React 中的副作用處理-初探 Effect * useEffect 其實不是生命週期 API --- ## React 中的副作用處理 **-初探 Effect** 什麼是 useEffect? <span> useEffect 是一個 React Hook<!-- .element: class="fragment" data-fragment-index="1" --></span> <span> 可以讓 component 與外部系統同步化<!-- .element: class="fragment" data-fragment-index="2" --></span> ---- 如何同步化? -- <div> 將 Component 以外的資料<!-- .element: class="fragment" data-fragment-index="1" --></div> <div> 同步化到 Component 內部<!-- .element: class="fragment" data-fragment-index="2" --></div> -- <div> 將 Component 內部的資料流與外部系統同步<!-- .element: class="fragment" data-fragment-index="3" --></div> ---- 對伺服器、後端API發出請求 <!-- 除了 get 以外,拿來同步 component 內部更新內部資料,也可能是 post 更新、修改等操作 --> 監聽某些事件 <!-- 監聽使用者觸發了 onScroll 滑動事件 --> 存取 React 外部的狀態管理套件 <span>"外部系統" => 是指某些 React 無法控制的程式碼 <!-- .element: class="fragment fragment-highlight-red" data-fragment-index="1" --></span> ---- ![image](https://hackmd.io/_uploads/B1iseUm40.png) <!-- * setInterval() and clearInterval() * 事件處理器訂閱: window.addEventListener 和 window.removeEventListener * 第三方動畫 Library API 像 animation:start 和 animation:reset() --> --- ## 什麼是副作用? side effect? ---- 函式 除了回傳一個結果值以外 還依賴或影響函式外部以外的其他東西 影響變數等等操作 ```===javascript let globalVariable = 0 function calculateDouble(number){ globalVariable +=1 fetch(/*....*/).then((res) => { /*....*/ }) document.getElementById('app').style.color = "red" return number*2 } ``` ---- 副作用聽起來不好? 但是有時候 我們就需要它的副作用的影響或效果, 同時的, 副作用可能會造成一些負面的影響。 * 可預期性降低 <!-- 帶有副作用的函式也會更難以預測行為 --> * 測試困難 <!-- 難以進行單元測試,因為涉及外部資源或狀態,難以模擬或隔離外部因素來做測試 --> * 高耦合度 <!-- 副作用往往增加系統各部分之間的緊密程度,會增加修改或重構的困難度。 --> * 難以維護和理解 <!-- 通常在查看包含有副作用的程式,它跟上下文和其他程式碼都有相連關係,會比較難理解或修改。 --> * 優化限制 ---- 若在 Component Fn 中 直接處理副作用程式碼 一些累加可能拖慢 React Element 的產生 可能影響到使用者體驗差 <!-- 就無法留住人停留在該網頁 --> ![1716939136221](https://hackmd.io/_uploads/ryx6tkENC.png) ---- 我們可使用 useEffect 隔離副作用 使之在每次 render 更新完成後才執行 就不會有機會造成 Component fn 產生 React Element 時被阻塞 避免副作用的處理直接阻塞畫面的產生與更新 ---- 所以我們需要 useEffect <span> 來處理副作用<!-- .element: class="fragment" data-fragment-index="1" --></span> <span> 清除或逆轉產生的副作用影響<!-- .element: class="fragment" data-fragment-index="2" style="color:yellow"--></span> <span> 避免副作用疊加所造成的不可預期性的錯誤<!-- .element: class="fragment" data-fragment-index="3" style="color:yellow"--></span> ---- 如何清除副作用? * 當 component 隨著每次 re-render, 需重複執行副作用後,(若有產生)都會需要清掉前次產生的副作用。 * 解決方式: => 回傳 cleanup fn,透過建立 cleanup fn 來清除副作用所造成的影響。 * 時機點: 會在每次副作用執行前,或 unMount 的時候被執行。 ---- 若有副作用的產生 但沒使用 cleanup fn 清除副作用產生的影響 可能 component 被 unmount 還在持續觸發事件 導致 memory leak 以及效能浪費等問題 ---- 什麼是 memory leak (記憶體洩漏)? <span> 當一段程式碼不在使用某些記憶體,但該段記憶體卻未被適時釋放, 導致記憶體無法被其他程式使用或重新分配 導致系統可使用記憶體越來越少。 <!-- .element: class="fragment" data-fragment-index="1" style="size=10" --></span> <!-- Javascript 語言本身雖有自動的垃圾回收機制,但若未有 cleanup 函式將對未使用 connection 解除綁定的話,它會依然保持 connection 並在有 connection變動時不斷被觸發相對應的 callback 函式,就算 component 被 unMount 還是會持續占用記憶體空間來存放,並且浪費效能去執行它。 除非應用程式被關閉並重新執行,不然對此占用會是持續性,且越累積越多--> ---- 與外部伺服器連線,建立 connection ```===javascript import { useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); // ... } ``` --- useEffect(effectFunction, dependencies?) effect fn 若無回傳 cleanup fn, effect fn 回傳 undefined cleanup fn <span>清除副作用所造成的影響<!-- .element: class="fragment" data-fragment-index="1" style="color:yellow"--></span> dependencies 可選填的陣列參數 <span>優化效能<!-- .element: class="fragment" data-fragment-index="2" style="color:yellow"--></span> ---- 以 useEffect 來定義並管理一段副作用 可分成此三大步驟: 1. 定義一個 effect 函式來處理副作用 2. 加上 cleanup 函式來清除副作用所帶來的影響(若有需要的話) 3. 指定 effect 函式裡的依賴陣列,以跳過某些不必要的副作用處理,避免效能浪費 --- Dependencies 參數 * 用來告知 React effect 函式是否可以在某些次 re-render 時跳過執行。 * 陣列內放的每一個數值會經過<span> `Object.is()`<!-- .element: class="fragment" data-fragment-index="1" style="color:yellow"--></span> 判斷 數值是否有被更新,若無更新,此次可安全跳過執行此副作用<span>可避免浪費效能<!-- .element: class="fragment" data-fragment-index="2" style="color:red"--></span> * dependencies 陣列應包含 effect fn 在此 component 所依賴影響的資料項目。 <span>如: props, state, 或任何受到資料流所影響的延伸資料。<!-- .element: class="fragment" data-fragment-index="3" style="color:red"--></span> ---- <span>是用來控制執行時機?<!-- .element: class="fragment" data-fragment-index="1" style="color:red"--></span> * 直接不提供 dependecies 參數 => 每一次 re-render 都要執行副作用 <span>沒有 dependenecies 幫助優化的陣列參數,每次都要執行副作用<!-- .element: class="fragment" data-fragment-index="2" style="color:yellow"--></span> * 在 dependencies 提供一個空陣列 => 僅有初次 render 時執行此副作用,re-render 都可以跳過執行 <span>有 dependenecies 幫助優化的陣列參數,但依賴需執行的變數不存在,故只有初次渲染要執行副作用<!-- .element: class="fragment" data-fragment-index="3" style="color:yellow"--></span> --- Component 每次 render 都有其自己版本的 props, state, event handlers, effect 函式, cleanup 函式, 會透過 closure 的特性, 捕捉並記住該次 render 中 props 和 state 的快照。 [repo 範例](https://codesandbox.io/p/sandbox/useeffect-render-example2-vywz9w?file=%2Fsrc%2FSubscriptionComponent.js%3A6%2C53) ---- <!-- .slide:data-background-color="#fff" --> ```flow! st=>start: 首次 Render e=>end: 結束 op=>operation: 從 useState 取得目前的 state值為1 作為快照值 op2=>operation: 以目前的資料流建立 event handler, effect fn, cleanup fn op3=>operation: 以state 快照值產生新的畫面 React Element 綁定資料(event handler) op4=>operation: 轉換 RE, 並繪製到實際的DOM上 op5=>operation: 執行 effect 函式 op6=>operation: (首次render跳過)執行前次 cleanup 函式 st->op->op2->op3->op4->op6->op5 ``` ---- <!-- .slide:data-background-color="#fff" --> ```flow! st=>start: 經過一次按鈕點擊,執行setCurrentOrder(currentOrder + 1),觸發 re-render e=>end: 結束 op=>operation: 執行到 useState時,React 在內部執行之前呼叫 setState的動作。 將待計算動作完成,並將 state的值取代更新為 1+1= 2 op1=>operation: 從 useState 取得目前的 state 作為快照值 op2=>operation: 以目前的資料流建立 event handler, effect fn, cleanup fn op3=>operation: 以state 快照值產生新的畫面 React Element 綁定資料(event handler) op4=>operation: 轉換 RE, 並繪製到實際的DOM上 op5=>operation: 執行 effect 函式 op6=>operation: 執行前次 cleanup 函式 st->op->op1->op2->op3->op4->op6->op5 ``` --- React 中的副作用處理: effect 初探 章節重要觀念自我檢測 `---` * 什麼是副作用?為什麼我們需要透過 `useEffect` 在 React component function 中處理副作用? * 以 `useEffect` 來處理一段副作用的三大步驟是什麼? ---- React 中的副作用處理: effect 初探 章節重要觀念自我檢測 `---` * 解釋「每次 render 都有其自己版本的 effect 與 cleanup 函式」是什麼意思? * 一次 render 中的 effect 函式與 cleanup 函式會在什麼時間點被執行? --- ## useEffect 是宣告式的同步化 而非生命週期API `----` useState 方法更新資料 <!-- react 會以最新的資料重新 render component,將原始資料同步化到畫面結果--> useEffect 的用途: 是將 "原始資料" 同步化到畫面以外的副作用處理上 ---- 宣告式與指令式程式設計 React vs. JavaScript 宣告式: 只關注結果,不管如何達成目的 指令式: 一一步驟告知執行的順序,來達到結果 ---- useEffect 也是以宣告式的概念來設計 <!-- useEffect 讓你根據目前的 props 與 state 資料來同步化那些與畫面無關的其他東西,也就是副作用的處理 --> <!-- useEffect 不是 function component 生命週期的 API--> <!-- 執行時機看起來與 class component 中的 componentDidMount 以及 componentDidUpdate 類似 --> 但其實 useEffect 副作用處理 將原始資料同步化到 React Element 以外的東西上 無論重複 render 了幾次 資料流與程式邏輯都應該保持同步化與正常操作 ---- 為什麼要以 useEffect 的資料流 同步化取代生命週期API? <span> 什麼是生命週期API? <!-- .element: class="fragment" data-fragment-index="1" --></span> <span>[React Component Lifecycle - Hooks / Methods Explained](https://www.youtube.com/watch?v=m_mtV4YaI8c)<!-- .element: class="fragment" data-fragment-index="2" --></span> ---- class component 生命週期API: 1. constructor: 建立 component 2. componentDidMount: component 首次 render 執行 3. componentDidUpdate(prevProps, prevState, snapshot?): component re-render 後執行 4. componentWillUnMount: component 被 unMount,移除前執行 5. shouldComponentUpdate(nextProps, nextState): 通常用來優化性能,預設返回 true,若返回 false 可以防止 component 被 re-render ---- class component 生命週期API: 6. getDerivedStateFromProps(props, state): React 會在render(初次render,或更新re-render)前執行。它會回傳 object 來更新 state,或是 null 不執行更新。 7. getSnapShotBeforeUpdate(prevProps, prevState): React 在 React 更新 DOM之前呼叫它. 讓 component 抓取 DOM 在更新以前的資訊 (e.g. scroll 位置) 回傳值會傳入componentDidUpdate 的參數 9. componentDidCatch(error,info): 當發生錯誤時,React 會按照定義錯誤訊息顯示在畫面上。 ---- Component 初次 render 訂閱狀況 ```javaccript= componentDidMount(){ OrderAPI.subscribeStatus( this.props.id this.handleStatusChange ) } componentWillUnMount(){ OrderAPI.unsubscribeStatus( this.props.id this.handleStatusChange ) } ``` ---- componentDidUpdate 不會自動抓取前次的 id 取消訂閱 也不會自動以新的 id 來重新訂閱 => 進而造成 memory leak 等問題 <!-- 忘記正確處理 componentDidUpdate 正是 class component 常見的 bug 來源 --> ```javaccript= componentDidMount(){ OrderAPI.subscribeStatus( this.props.id this.handleStatusChange ) } componentDidUpdate(prevProps){ OrderAPI.unsubscribeStatus( prevProps.id this.handleStatusChange ) OrderAPI.subscribeStatus( this.props.id this.handleStatusChange ) } componentWillUnMount(){ OrderAPI.unsubscribeStatus( this.props.id this.handleStatusChange ) } ``` <!-- 這是處理一個副作用,想像若是同個 component 要同時處理多個副作用,需要再生命週期API裡面分別添加邏輯,程式碼就很容易互相打架壞掉 --> ---- <!-- class component 的 didMount, didUpdate, willUnMount 會需要開發者將這些處理情境拆開思考,去決定在這些情境要分別執行那些動作,副作用處理是會被散落在不同的地方,將他們正確的一一寫入,並正確的組合再一起才能做到正確地將資料同步化到副作用處理上。 --> useEffect 從 didMount + didUpdate + willUnMount 中 提取其中副作用來統一整合 <!-- 類似的情境 --> <span> 只需要一個 useEffect 搞定 <!-- .element: class="fragment" data-fragment-index="3" style="color:yellow"--></span> 只要定義一段描述同步化的邏輯 然後再按需求加上 cleanup function 去清除可能造成的副作用影響。 --- ## Dependencies 是一種效能優化 -- 而非執行時機的控制 ```javascript= import { useState, useEffect } from "react" export default function App(props){ const [count, setCount] = useState(0) useEffect(() => { document.title = `Hello ${props.name}` },[props.name]) return ( <button onClick={()=> setCount(count + 1)}> Click me </button> ) } ``` ---- 加入 dependencies 流程模擬: * 首次 render 時,確認資料流 props.name = 'foo' 與 state count 的值為 0 1. 渲染出 React Element 產生相對應的 DOM 結構 2. 執行本次 render 對應的 effect 函式,以及對應本次 props.name 的值為'foo' 3. 記憶此 useEffect 的 dependencies 裡的 props.name 值為'foo',作為下次 effect 函式是否執行的比較依據。 ---- * 經過使用者點擊觸發 setCount,觸發re-render 後,確認資料流 props.name = 'foo' 與 state count 的值為 0 + 1 = 1 1. 比較 React Element 差異,找出要更新的RE,畫面產生相對應的 DOM 處理 2. 檢查 useEffect 的 dependencies 陣列的每一個項目值,找到為一個 props.name,比較與前一次 render 時是否相同 (使用 Object.is() 判斷) 3. 確認相同,可跳過執行此 useEffect 函式 ---- * 若父 Component render 改變了 props.name 為 "bar",觸發子 component 第三次 re-render,state count 的值仍為 1 1. 比較 React Element 差異,找出要更新的RE,畫面產生相對應的 DOM 處理 2. 檢查 useEffect 的 dependencies 陣列的每一個項目值,找到為一個 props.name,比較與前一次 render 時是否相同 (使用 Object.is() 判斷) ---- 3. 前一次版本的 props.name "foo",已經更新為 "bar",須照常執行本次 render 的 effect 函式。 4. 執行本次 props.name 值為 "bar" 的 effect 函式 5. 記憶此 useEffect 中的 dependencies 陣列中所有變數的值,也就是 props.name 值為 "bar" ,做為下一次依賴相比較的依據。 ---- dependencies 能不能放 Object 和 Fn? 放原始型別與 Object 的差異? ![image](https://hackmd.io/_uploads/rkSb45NVC.png) <!-- 盡可能地減少不必要的 Object dependencies,若真的需要可以將它移到 effect 函式的內部,改監控 roomId number 型別就好 --> --- 若沒有任何依賴項,則可以填寫空陣列 <span>但不可以欺騙 React 來試圖控制 effect 函式執行時機<!-- .element: class="fragment" data-fragment-index="1" style="color:yellow"--></span> <span>應要改用條件判斷<!-- .element: class="fragment" data-fragment-index="2" style="color:red"--></span> <span>這個 effect 函式執行的時機<!-- .element: class="fragment" data-fragment-index="3" style="color:red"--></span> <!-- 讓你的副作用處理即使根本沒提供 dependencies 參數,也能在每次 render 時都能保持程式碼正確的執行 --> --- useEffect 其實不是生命週期的API 章節重要觀念自我檢測 * `useEffect` 是 function component 的生命週期 API 嗎?為什麼? * 為什麼 React 要以 `useEffect` 的資料流同步化這種設計來取代生命週期 API? * `useEffect` 的 `dependencies` 機制的設計目的與用途是什麼? * 我們可以用 `dependencies` 參數來模擬 function component 生命週期 API 的效果嗎? --- # The End
{"slideOptions":"{\"transition\":\"slide\"}","title":"React 思維進化","contributors":"[{\"id\":\"aef1372a-7cb3-4de4-bc8d-affbed099920\",\"add\":22981,\"del\":11268}]","description":"React 中的副作用處理-初探 Effect"}
    486 views