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