---
# System prepended metadata

title: React Hook 筆記
tags: [前端]

---

## 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;
```