## React Hook 介紹 :::info :bulb: **React Hook 可以讓 function component (函式元件) 也具備像 class component 生命週期的能力。** :bulb: **React Hook 可以根據程式邏輯拆分成多次呼叫。** 即不會受到生命週期或拆分程式邏輯影響。 ::: ### Hook 使用規則 1. 只能在 React Function Component 中呼叫 Hook,Class Component 是不能用 Hook。 2. 只能於組件的 **最頂層呼叫**:不要在 **迴圈、條件式或是巢狀的 function 內** 呼叫 Hook,這樣才能確保程式中每一次 component render 時 Hook 被呼叫順序都要是一樣的。 ### useState 鉤子 範例程式碼: ```javascript const [state, setState] = useState(initialState); // 以箭頭函式執行初始化,只會執行一次 const [state, setState] = useState(() => { // code ... return initialState; }); ``` #### 使用說明 1. useState 會回傳當前的 state 及一個用來更新 state 的函式(setState)。 2. 首次渲染會用 initialState(初始值),後續則使用最近設定的 state 值渲染。 3. setState 為 **「批次更新」**,會在下次 batch re-render 一併更新元件狀態。 - Batching of state updates:(批次更新 => 改善效能) 3.1. React 不會在每次 setState 後就馬上同步地執行 re-render,而是將多個 setState 累積加入到 queue 中, 3.2. 於一次 re-render 階段更新多個 state,藉由批次處理 state updates 就只要 re-render 一次來改善效能。 4. 當 state 改變,便會觸發 re-render。 ### useEffect 鉤子 範例程式碼: ```javascript! const App: React.FC<IApp> = (props) => { useEffect(() => { // do something ... return () => { // cleanup function }; }, [dependencies]); } ``` #### 使用說明 1. 若**無**設定第二個參數(即**無逗號**),則於組件每次渲染完後被呼叫執行。 2. 若**有**設定第二個參數,只要每次重新渲染後 dependencies 陣列內的元素沒有改變,則任何 useEffect 裡面的函式就不會被執行。 3. 若第二個參數為空陣列,則組件重新渲染後 <font color="red">**執行 1 次**</font> 後,**就不會再執行**。 4. 回傳一個函式即清除函式,用於 **註銷或解除** 元件銷毀後將不再使用的事件或資源。 5. 可實現 Class Component 中 componentDidMount()、componentDidUpdate() 與 componentWillUnmount() 生命週期函式。 #### 使用場景 當 React 更新完 DOM 後,需要執行其他程式(side effect),如網路請求、操作 DOM 等,就可以使用 useEffect 鉤子。 :::success :bulb: cleanup 函式的執行時機點 - **update 階段:** 1. (React 16) <font color='blue'>**執行 cleanup 函數**</font> → 畫面更新(re-render) → **執行 effect** 2. (React 17) 畫面更新(re-render) → <font color='blue'>**執行 cleanup 函數**</font> → **執行 effect** - **unmount 階段:** 1. (React 16) 元件更新 → <font color='blue'>**執行 cleanup 函數**</font> → DOM 作對應改變 → 畫面更新(re-render) 2. (React 17) 元件更新 → DOM 作對應改變 → 畫面更新(re-render) → <font color='blue'>**執行 cleanup 函數**</font> :warning: React 17 於各階段 cleanup 函式皆改到 **畫面更新之後** 才執行。 ::: ### useCallback 鉤子 範例程式碼: ```javascript! const App: React.FC<IApp> = (props) => { const fn = useCallback(() => { // do something ... }, [dependencies]); } ``` #### 使用說明 1. 第一個參數為 **箭頭函式**,可將函式邏輯放於此處。 2. 第二個參數為 **此函式感知變動的相依陣列**:相依元素變動,則函式實體 **重新生成**。 3. useCallback 回傳值可作為其他 useEffect 的相依元素。 #### 使用場景 暫存函式實體,若渲染時 **相依陣列內的元素發生改變**,才會生成並回傳新的函式實體,否則就維持舊的函式實體(不重新生成),可作為 **效能最佳化** 的手法之一。 :::success 每次 render 時,元件內的函式實體都會重新產生,useCallback 可以改善此函式實體可藉由 **感知資料流變化** 才發生改變。 ::: :::danger **誤區說明** :warning: 通常與 **shouldComponentUpdate/React.memo** 搭配使用,才可真正節省效能:透過 React.memo 包裝的元件可節省在 **相依參數不變** 的狀況下因傳入的函式實體一直變更而產生 **重複渲染** 效能。 一般元件應該不需要使用。 ::: ### useRef 鉤子 #### React 官方定義 ><font color='blue'>useRef</font> is a React Hook that lets you reference a value **that’s not needed for rendering.** :::success :bulb: 回傳一個 Ref 物件(可為值、DOM 元素或計算結果),當 Ref 若發生變更,則 **不會觸發** 畫面更新(re-render)。 ::: #### React 使用場景 1. **Referencing a value with a ref(使用 ref 對照值)** 可以使用 useRef <font color='orange'>建立一個可變的值</font>,且數值變更後不會引發重新渲染。 3. **Manipulating the DOM with a ref(使用 ref 操作 DOM )** 可以使用 useRef 來直接操作 DOM 元素。通過將 ref 對象的 .current 屬性<font color='orange'>設置為一個 DOM 節點</font>,可以直接讀取或修改這個節點。 5. **Avoiding recreating the ref contents(避免不必要的重建 ref 內容)** 使用 useRef 可以幫助你避免在重新渲染 component 時不必要地重建 ref 的內容,<font color='orange'>因為 ref 會在 re-render 中保持不變</font>,例如一些資源很大的影音 component 就可以使用此方法,避免重複拿取資源、更新畫面。 ### React.memo 高階元件 (HOC) 範例程式碼: ```javascript! // memoSample.ts import React from 'react'; import { ISample } from './types'; const Sample: React.FC<ISample> = (props) => { return ( <> <div>Hi, {props.name}</div> </> ); } const MemoSample = React.memo(Sample); export default MemoSample; ``` #### 使用說明 1. 包入 React.memo 的 React 元件會 **判斷傳入 props 是否發生改變**,而決定是否觸發畫面更新(re-render),可作為 **效能最佳化** 的手法之一。 2. 若是傳入的 props 中有 **函式** 參數,若此函式實體於每次渲染都不同的話,則不會有效能最佳化的效果(每次都會重新渲染),但可用 useCallback 解決此問題。 ```javascript! // memoSample.ts import React from 'react'; import { ISample } from './types'; const Sample: React.FC<ISample> = (props) => { return ( <> <div>Hi, {props.name}</div> <button onClick={props.showAlert}>alert!</button> </> ); } const MemoSample = React.memo(Sample); export default MemoSample; ``` ```javascript! // app.ts import React from 'react'; import MemoSample from './memoSample'; const App: React.FC = (props) => { // 用 useCallback 包住可以起到效能最佳化的效果! const showAlert = useCallback(() => alert('hi'), []); return ( <MemoSample name="Sam" showAlert={showAlert} /> ); } export default App; ``` ### useMemo 鉤子 用法與 useCallback 相似,可用來記憶 **陣列或物件類型** 的資料集合,或是 **複雜的計算結果**。 ```javascript! // sample.ts import React from 'react'; import { ISample } from './types'; const Sample: React.FC<ISample> = (props) => { return ( <> <div>Hi, {props.name}</div> {props.numbers.map(it => ( <h4>{it}</h4> ))} </> ); } const MemoSample = React.memo(Sample); export default MemoSample; ``` ```javascript! // app.ts => 無效能最佳化 import React from 'react'; import MemoSample from './sample'; const App: React.FC = () => { // 每次畫面更新必定為新的參考! const numbers = [1, 2, 3]; // effect 的 deps 效能最佳化永遠都會失敗 useEffect(() => { console.log(numbers); }, [numbers]); // <MemoSample> 的 render 快取效能最佳化永遠都會失敗 return ( <MemoSample name="Sam" numbers={numbers} /> ); } export default App; ``` ```javascript! // app.ts => 有效能最佳化 import React from 'react'; import MemoSample from './sample'; const App: React.FC = () => { // 用 useMemo 包住陣列 const numbers = useMemo( () => [1, 2, 3], [] ); // effect 的 deps 效能最佳化可以正常運行 useEffect(() => { console.log(numbers); }, [numbers]); // <MemoSample> 的 render 快取效能最佳化可以正常運行 return ( <MemoSample name="Sam" numbers={numbers} /> ); } export default App; ```