# React Hooks Note ## Roles ### 只在最上層呼叫 Hook ( React function top level ) **不要在迴圈、條件式、嵌套 ( nested ) function 內呼叫 Hook** component render 時,Hook 呼叫的順序: ``` typescript function Form() { // 1. 使用 name state 變數 const [name, setName] = useState('Mary'); // 2. 使用一個 effect 來保存表單 useEffect(function persistForm() { localStorage.setItem('formData', name); }); // 3. 使用 surname state 變數 const [surname, setSurname] = useState('Poppins'); // 4. 使用一個 effect 來更新標題 useEffect(function updateTitle() { document.title = name + ' ' + surname; }); // ... } ``` ``` typescript // ------------ // 第一次 render // ------------ useState('Mary') // 1. 用 'Mary' 來初始化 name state 變數 useEffect(persistForm) // 2. 增加一個 effect 來保存表單 useState('Poppins') // 3. 用 'Poppins' 來初始化 surname state 變數 useEffect(updateTitle) // 4. 增加一個 effect 來更新標題 // ------------- // 第二次 render // ------------- useState('Mary') // 1. 讀取 name state 變數 (參數被忽略了) useEffect(persistForm) // 2. 替換了用來保存表單的 effect useState('Poppins') // 3. 讀取 surname state 變數 (參數被忽略了) useEffect(updateTitle) // 4. 替換了用來更新標題的 effect // ... ``` 只要 Hook 在 render 時被呼叫的順序是一致的,React 可以將一些 local state 和它們聯繫在一起。 - 錯誤 ❌ ``` typescript // 🔴 違反了第一個規則,在條件式中使用 Hook if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); } ``` name !== '' 條件式在初次 render 時為 true,所以 Hook 被執行。然而,在下一次 render 時**使用者可能清除了表單**,使得條件式變為 false。所以現在的 render **跳過**了這一個 Hook。 Hook 的呼叫順序因此改變: ``` typescript // ------------ // 第一次 render // ------------ useState('Mary') // 1. 用 'Mary' 來初始化 name state 變數 useEffect(persistForm) // 2. 增加一個 effect 來保存表單 useState('Poppins') // 3. 用 'Poppins' 來初始化 surname state 變數 useEffect(updateTitle) // 4. 增加一個 effect 來更新標題 // ------------- // 第二次 render // ------------- useState('Mary') // 1. 讀取 name state 變數 (參數被忽略了) // useEffect(persistForm) // 🔴 這個 Hook 被跳過了! useState('Poppins') // 🔴 2 (但之前是 3). 未能讀取 surname state 變數 useEffect(updateTitle) // 🔴 3 (但之前是 4). 未能取代 effect // ... ``` React 不知道第二個 useState Hook 呼叫的回傳值是什麼。React **預期**在這個 component 中的第二個 Hook **呼叫對應的** persistForm effect,就和**前一次 render** 相同。在跳過的 Hook 後面,每個 Hook 呼叫都會 shift 一個,導致 bug 的發生。 ***這就是必須在 component 的上層 ( top level ) 來呼叫 Hook的原因*** - 正確✅ ``` typescript useEffect(function persistForm() { // 👍 不再違反第一個規則 if (name !== '') { localStorage.setItem('formData', name); } }); ``` ### 只在 React Function 中呼叫 Hook **不要在一般的 JavaScript function 中呼叫 Hook** - ✅ 在 React function component 中呼叫 Hook - ✅ 在自定義的 Hook 中呼叫 ## Hooks ### useState ``` typescript // initial state const [state, setState] = useState(initialState); // set new state setState(newState); ``` - 回傳一個 state 的值,以及更新 state 的 function - 首次 render 時,回傳的 state 的值會跟第一個參數(initialState)一樣 - setState function 用來更新 state: 接收一個新的 state ( newState ) 並將 component 的 re-render 排進序列 - 後續的 re-render,useState 回傳的第一個值**必定會是最後更新的 state** - setState function 本身是穩定的,而且**不會在 re-render 時改變**。所以可以安全地從 useEffect 或 useCallback 的依賴列表省略 - 如果 update 函式回傳與目前的 state 相同的值,後續的 re-render 將會被完整跳過 #### useState 不會自動合併更新 object,但可以這麼做: ``` typescript const [state, setState] = useState({}); setState(prevState => { // 也可以使用 Object.assign return {...prevState, ...updatedValues}; }); ``` #### useState 初始宣告可以給一個 function: ``` typescript const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; }); ``` #### 跳過 state 更新 - 如果使用與目前 state 相同值來更新 State Hook,React 將會**跳過子 component 的 render 及 effect 的執行**(React 使用 [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#description) 來比較) - React 可能仍需要在跳過 render 之前 render 該 component。這應該不是問題,因為 React 不會不必要地「深入」到 component tree 中。如果在 render 當中執行了很多程式碼或是複雜的計算,可以使用 useMemo 來最佳化 - [Batching of state updates](https://zh-hant.reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching) ### useEffect - 接受一個包含命令式,且可能有 effectful code 的 function。 - 在 function component(React 的 render 階段)的 function 內,mutation、subscription、timer、日誌記錄、以及其他 side effect 是不被允許的。因為這可能會導致容易混淆的 bug 和不一致的 UI。 - 如果使用 useEffect,傳遞到 useEffect 的 **function 會在 render 到螢幕之後執行**。可以把 effect 看作 React 從純函式 ( functional ) 通往指令式 ( imperative 命令 or procedural 過程 ) 的跳脫方式 - functional language 函數式程式的設計範式 ( Paradigm: 可以說是一種模式 ) 是被明確地創建用來支援解決問題的純函數式方法,**將問題組合為一組要執行的函數**。函數式程式設計是宣告式 ( declarative ) 程式設計的一種形式。 - imperative language 開發人員編寫代碼來**指定(命令)計算機**為實現目標而必須採取的步驟,詳細定義每個函數的輸入以及每個函數傳回的內容。有時被稱為演算法程式設計。 - [difference](https://learn.microsoft.com/en-us/dotnet/standard/linq/functional-vs-imperative-programming) ![microsoft: functional and imperative](https://i.imgur.com/OYob6IX.png) - 在預設情況下,effect 會在每一個**完整 render 後執行**,也可以在某些**值改變的時候才執行**: use **[dependencies array](#有條件的觸發-effect)**。 #### 清除一個 effect - 通常在 component **離開螢幕**之前需要**清除 effect** 所建立的資源,傳遞到 useEffect 的 function 可以 return 一個清除的 function ``` typescript useEffect(() => { const element = document.getElementById(id: string); function handler() { //... } element.addEventListener("click", handler); return () => { // Clean up element listener when component leave screen element.removeEventListener("click", handler); }; }); ``` - 清除 function 會在 component 從 UI 被移除前執行,來防止 **memory leak** - 如果 component render 了數次(通常會這樣),在執行下一個 effect 前,上一個 effect 會被清除,**每次更新都會建立新的物件** #### Effect 的時機 - 在延遲事件期間,傳遞給 useEffect 的 function 會在 **layout 和 render 之後觸發**,這使它適用於很多常見的 side effect,例如設定 subscription 和 event handler,因為絕大部份的工作都不應該阻礙瀏覽器更新晝面 - 不是所有的 effect 都可以被延後。例如,使用者可見的 DOM 改變必須在下一次繪製之前同步觸發,這樣使用者才不會感覺到視覺不一致(概念上類似被動和主動 event listener 的區別) - 這類型的 effect,React 提供了一個額外的 **useLayoutEffect** Hook - 結構和 useEffect 相同,只是執行的時機不同 - React 18 開始,傳給 useEffect 的 function 將在 layout 和 paint 之前 同步的觸發,當它是一個離散的使用者輸入(像是點擊)或被 wrap 在 flushSync 的更新結果。這個行為讓 event system 或 flushSync 的 caller 觀察 effect 的結果 - 只會影響傳遞給 useEffect 的 function 被呼叫的時間 - 在這些 effect 中安排的更新仍然會被延遲。這與 useLayoutEffect 不同,後者會觸發 function 並立即處理其更新 - 雖然 **useEffect 會被延遲直到瀏覽器繪制完成**,但會保證在任何新 render 前執行。React 會在開始新一個更新前刷新上一輪 render 的 effect。 #### 有條件的觸發 effect - effect 的預設行為是在每次完成 render 後觸發 effect。如果其中一個**依賴 (dependencies)** 有改變,則會重新建立一個 effect - 例如,需要在 source prop 改變後才重新建立 subscription,而不需要在每次更新後,可以給 useEffect 第二個參數,依賴列表 ( dependencies array ) ``` typescript useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source], ); ``` - 確保 dependencies array 包含了**所有在該 component 中會隨時間而變的值(例如 props 和 state)以及在該 effect 所使用到的值**。否則,程式碼會引用先前 render 的舊變數 - [如何處理 function](https://zh-hant.reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies) - [array 的值頻繁變化時](https://zh-hant.reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often) - 如果想讓 effect 只執行和清除一次(在 mount 和 unmount),可以傳遞一個空的 array ( [] ) 作為第二個參數。這告訢 React effect 沒有依賴任何 props 或 state,所以它永遠不需要再次被執行。這並不是一個特殊處理 — 它依然遵循依賴 array 的運作方式。 - 如果傳入了一個空的 array ([]),effect 內部的 props 和 state 就一直擁有其初始值 - 有點類似 componentDidMount 和 componentWillUnmount 的思維模式 - **建議使用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則**。它會在依賴錯誤時發出警告並提出修正建議 - dependencies array 並不作為傳到 effect function 的參數。但從概念上來説,**所有在 effect function 中引用的值都應該出現在依賴 array 中**。在未來,一個足夠先進的編譯器可以自動建立這個 array ### useContext ``` typescript const value = useContext(MyContext); ``` - 接受 context object (React.createContext return value),並回傳目前 context value - Context 目前的值是取決於由上層 component 距離最近的 <MyContext.Provider> 的 value prop ### useReducer