# 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)

- 在預設情況下,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