---
# System prepended metadata

title: React Hooks Note

---

# 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

