---
# System prepended metadata

title: 效能優化：useCallback、useMemo、useReducer

---

# 效能優化：useCallback、useMemo、useReducer

# 優化1：`useCallback` 的使用情境
> 當組件用到 不是每次都會改變 的function時。

以fetch為例：

```jsx
function SearchResults() {
  const [query, setQuery] = useState('react');

  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;
    }

    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, [query]); // ✅ Deps 是 OK 的

  // ...
}
```

[code](https://codesandbox.io/s/pwm32zx7z7?file=/src/index.js:0-887)

## 但我不想要把這個函式放進 Effect 裡

例如，好幾個在同個元件裡的 effect 可能會呼叫一樣的函式，你不想要複製貼上它的邏輯。

### 錯誤解法：單純抽出來，但還是寫在同一個function裡面

造成 infinite loop

```jsx=
function SearchResults() {
  // 🔴 在每次渲染都重新觸發所有 effect：infinite loop
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... 獲取資料和做一些事 ...
  }, [getFetchUrl]); // 🚧 Deps 是正確的但它們太常改變了

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... 獲取資料和做一些事 ...
  }, [getFetchUrl]); // 🚧 Deps 是正確的但它們太常改變了

  // ...
}
```
:::spoiler 完整的code


```jsx =
import React, { useState, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
import axios from "axios";

function SearchResults() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("react");

  function getFetchUrl(query) {
    return "https://hn.algolia.com/api/v1/search?query=" + query;
  }

  useEffect(() => {
    console.log(" getFetchUrl('react')");
    const url = getFetchUrl("react");
    // ... 獲取資料和做一些事 ...
    async function fetchData() {
      const result = await axios(url);
      setData(result.data);
    }
    fetchData();
  }, [getFetchUrl]); //  🚧 Deps 是正確的但它們太常改變了

  useEffect(() => {
    console.log(" getFetchUrl('redux')");
    const url = getFetchUrl("redux");
    // ... 獲取資料和做一些事 ...
    async function fetchData() {
      const result = await axios(url);
      setData(result.data);
    }
    fetchData();
  }, [getFetchUrl]); //  🚧 Deps 是正確的但它們太常改變了

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<SearchResults />, rootElement);
```
:::

### 解法1：抽到元件外層

如果一個函式不使用任何在元件範圍裡的東西，你可以把它抽到元件外層，然後自由地在 effect 裡使用它

```jsx
const [query, setQuery] = useState("react");

// ✅ 不會被資料流影響
function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... 獲取資料和做一些事 ...
  }, []); // ✅ Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... 獲取資料和做一些事 ...
  }, []); // ✅ Deps are OK

  // ...
}
```

:::spoiler 完整的code

```jsx
import React, { useState, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
import axios from "axios";

function getFetchUrl(query) { //抽到元件外層
  return "https://hn.algolia.com/api/v1/search?query=" + query;
}

function SearchResults() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("react");

  useEffect(() => {
    console.log(" getFetchUrl('react')");
    const url = getFetchUrl("react");
    // ... 獲取資料和做一些事 ...
    async function fetchData() {
      const result = await axios(url);
      setData(result.data);
    }
    fetchData();
  }, []); // ✅ Deps are OK

  useEffect(() => {
    console.log(" getFetchUrl('redux')");
    const url = getFetchUrl("redux");
    // ... 獲取資料和做一些事 ...
    async function fetchData() {
      const result = await axios(url);
      setData(result.data);
    }
    fetchData();
  }, []); // ✅ Effect deps are OK

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<SearchResults />, rootElement);
```
:::

### 解法2：使用 `useCallback`

```jsx
const [query, setQuery] = useState("react");

function SearchResults() {
  // ✅ 當他自己的 deps 一樣時，保留了特性
  const getFetchUrl = useCallback((query) => { //使用useCallback
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ✅ Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... 獲取資料和做一些事 ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... 獲取資料和做一些事 ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}
```

或是可以把query寫在dependencies裡面

```jsx
const [query, setQuery] = useState("react");

function SearchResults() {
  // ✅ 當他自己的 deps 一樣時，保留了特性
  const getFetchUrl = useCallback(() => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, [query]);  // ✅ Callback deps are OK: deoendencies裡面的query是useState定義的query

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... 獲取資料和做一些事 ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... 獲取資料和做一些事 ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}
```

[code](https://codesandbox.io/s/laughing-cray-ls8s2?file=/src/index.js)

# 優化2：useMemo的使用情境

> 有複雜的計算但是不是每次都需要重新計算的時候，例如：計算 n!

```jsx
import { useState } from 'react';

export function CalculateFactorial() {
  const [number, setNumber] = useState(1);
  const [inc, setInc] = useState(0);

  const factorial = factorialOf(number); //每次render時都會重新計算一次

  const onChange = event => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc(i => i + 1);
  
  return (
    <div>
      Factorial of 
      <input type="number" value={number} onChange={onChange} />
      is {factorial}
      <button onClick={onClick}>Re-render</button>
    </div>
  );
}

function factorialOf(n) { // 計算 n!
  console.log('factorialOf(n) called!');
  return n <= 0 ? 1 : n * factorialOf(n - 1);
}
```
:::danger
效能問題：重複計算是不需要的。且當數字很大時，計算會非常耗費效能。
:::

## 只在要計算的變數更新時才重新計算

### `useMemo`

> Returns a memorized value.

因為 `useMemo` 是緩存 value，所以它需要**先執行一次**，把值存起來。

```jsx
import { useState, useMemo } from 'react';

export function CalculateFactorial() {
  const [number, setNumber] = useState(1);
  const [inc, setInc] = useState(0);

  const factorial = useMemo(() => factorialOf(number), [number]); //使用useMemo

  const onChange = event => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc(i => i + 1);
  
  return (
    <div>
      Factorial of 
      <input type="number" value={number} onChange={onChange} />
      is {factorial}
      <button onClick={onClick}>Re-render</button>
    </div>
  );
}

function factorialOf(n) {
  console.log('factorialOf(n) called!');
  return n <= 0 ? 1 : n * factorialOf(n - 1);
}
```

# 優化3：`useReducer` 的使用情境
> 不想因為 變數狀態改變 導致需要重新刪除和建立元件時

以計時器為例（優化方式有兩種：`functional updater form`、`useReducer`）

**當我們想要每秒把螢幕上顯示的數字+1時：**

```jsx
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("useEffect")
    const id = setInterval(() => {
      console.log("setInterval")
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

  return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);
```

[code](https://codesandbox.io/s/0x0mnlyq8l?file=/src/index.js)

:::danger
效能問題：因為 重複建立+清除計時器。
但實際上我們是希望只建立一次計時器，然後只更新count的值而已。
:::

## 只更新count的值

### 1. 使用 `setState` 的 functional updater form

```jsx=
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("useEffect");
    const id = setInterval(() => {
      console.log("setInterval");
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []); //從dependencies中移除count

  return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);
```

[code](https://codesandbox.io/s/q3181xz1pj?file=/src/index.js)

### functional updater form 的限制：當變數增加

> 如果我們有兩個狀態的變數，它們的值依賴於彼此，或是如果我們想要根據 props 來計算下一個 state，它並不能幫助到我們。

```jsx=
function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step); //多了一個step的變數
    }, 1000);
    return () => clearInterval(id);
  }, [step]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}
```

[code](https://codesandbox.io/s/zxn70rnkx?file=/src/index.js)

:::warning
效能問題：當 step 改變時，還是會重複建立+清除計時器
:::

## 2. 使用 `useReducer`：對複雜state進行狀態管理
[簡述 useReducer](https://hackmd.io/0tGqO7HOSHSBSNyxIVinHA)

```jsx=
import React, { useReducer, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    console.log("useEffect");
    const id = setInterval(() => {
      console.log("setInterval");
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]); //dependencies 從 step 改成 dispatch [!]

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);
```

[code](https://codesandbox.io/s/xzr480k0np?file=/src/index.js:0-1043)

### 為什麼改成dispatch就不會重新建立計時器

React 保證了 dispatch 函式在元件的生命週期裡是常數。所以上面的例子不需要重新訂閱區間。

## 參考資料
### useReducer、useCallback

[使用 useReducer 和 useCallback 解决 useEffect 依赖诚实与方法内置&外置问题](http://www.ptbird.cn/react-hook-usereducer-usecallback.html)

[[ReactDoc] React Hooks - useEffect | PJCHENder 未整理筆記](https://pjchender.dev/react/react-doc-use-effect-hooks/)

[useEffect 的完整指南](https://overreacted.io/zh-hant/a-complete-guide-to-useeffect/)

### useMemo

[How to Memoize with React.useMemo()](https://dmitripavlutin.com/react-usememo-hook/)