# 效能優化: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/)