簡單介紹優化 react 的另一個方法 - useMemo
useMemo 是 react 裡面一個用來讓 react 記住 value 的 hook。
上一篇有介紹到 react memo 可以記住一個 component,但是如果 component 的 props 是一個 object 或是 array,有可能會沒有效用。
import { useState, memo } from "react";
type Props = { data: { value: number } };
const One = memo(function One({ data }: Props) {
console.log("One render"); // 觀察元件是否 re-render
return <p>One number: {data.value}</p>;
});
const Two = memo(function Two({ data }: Props) {
console.log("Two render"); // 觀察元件是否 re-render
return <p>Two number: {data.value}</p>;
});
function App() {
const [number, setNumber] = useState(0);
const [count, setCount] = useState(0);
function handleNumber() {
setNumber(number + 1);
}
function handleCount() {
setCount(count + 1);
}
console.log("--------App render--------");
return (
<div>
<One data={{ value: number }} />
<Two data={{ value: count }} />
<button onClick={handleNumber}>add number</button>
<button onClick={handleCount}>add count</button>
</div>
);
}
稍微修改一下上一篇用到的範例,改成不是直接把 state 傳入而是改成用 object 的方式把資料傳下去。
會發現本來應該作用的 memo 不起作用了,這是因為每一次 re-render 執行到這個地方的時候都是重新建立了一個新的物件往下傳遞。
<One data={{ value: number }} /> // 建立新的物件
<Two data={{ value: count }} /> // 建立新的物件
所以這時候就會需要透過 useMemo 來幫忙了。
const cachedValue = useMemo(calculateValue, dependencies)
calculateValue
: 一個 function 會回傳進行處理過的 value,function 本身必須要是 pure 不能有 side effect,value 可以是任何的資料型別,function 不能接收任何的參數,當 dependencies
裡面的東西都沒有改變時不會執行。
dependencies
: 一個 array 裡面應該放著在 calculateValue
function 裡面有使用到的外部變數,跟其他的 dependencies
一樣 react 會在 re-render 的時候透過 Object.is()
一個個進行比較,當結果有 false 的時候便會執行 calculateValue
來取得正確的 value。
cachedValue
: calculateValue
經過計算的 value,如果 dependencies
沒有改變時會是之前的 value。
function App() {
const [number, setNumber] = useState(0);
const [count, setCount] = useState(0);
const newNumber = useMemo(() => {
return { value: number };
}, [number]);
const newCount = useMemo(() => {
return { value: count };
}, [count]);
function handleNumber() {
setNumber(number + 1);
}
function handleCount() {
setCount(count + 1);
}
console.log("--------App render--------");
return (
<div>
<One data={newNumber} />
<Two data={newCount} />
<button onClick={handleNumber}>add number</button>
<button onClick={handleCount}>add count</button>
</div>
);
}
在 App 裡面先用 useMemo 把 number 跟 count 做處理就可以解決每一次都 re-render 都產生新的 object 的問題。
這樣我們的 memo 就又有作用了。
useMemo 通常會用來審略比較複雜的計算,那問題來了什麼樣的計算可以算是複雜呢?
import { useState } from "react";
// 複雜計算,這裡用費波那契數列為例
function fib(n: number): number {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
function App() {
const [count, setCount] = useState(35); // 從 35 開始
const [number, setNumber] = useState(0);
const addCount = () => setCount((prev) => prev + 1);
const addNumber = () => setNumber((prev) => prev + 1);
// 用 console.time() 計算時間
console.time("fib");
const fibCount = fib(count); // 取得第 count 個費波那契數列
console.timeEnd("fib");
return (
<div>
<p>Count: {count}</p>
<p>fib Count: {fibCount}</p>
<p>Number: {number}</p>
<button onClick={addCount}>add count</button>
<button onClick={addNumber}>add number</button>
</div>
);
}
這邊用 費波那契數列 做範例,每次元件 re-render 的時候會需要重新計算 fibCount 的 value。
可以看到即使改變了 number 的數字時也會影響到畫面 re-render 的情形,這個時候就可以使用 useMemo 來改善這個問題。
// ...
console.time("fib");
// 用 useMemo 把計算過程包起來
const fibCount = useMemo(() => fib(count), [count]);
console.timeEnd("fib");
// ...
用 useMemo 把複雜的地方包起來,省略不必要的計算。
react 的文件裡面有寫到通常處理超過成千上萬筆的資料才會算是昂貴的計算,又或者我們可以透過 console.time()
的 method 來檢查,像這樣。
console.time('do something');
const newCount = useMemo(() => {
return { value: count };
}, [count]);
console.timeEnd('do something');
如果出現的結果是 do something: 1.0 ms
那就可以透過 useMemo 來做 value 的 cache。
另外 useMemo 並不會讓我們第一次 render 的速度變快,他的用途是用來減少 re-render 中非必要的計算。
不是每一個跟 value 都要透過 useMemo 來 cache,可以在下面幾種情況裡面使用 useMemo 。
calculateValue
裡面的計算明顯影響到 re-render 的速度或結果,且 dependencies
裡面的變數不會經常改變。dependencies
所以為了減少其他 react hook 的執行次數,可以使用 useMemo 處理 value。useMemo 還有另外一個用途,上面有提到他可以回傳任何 value,所以 jsx 也可以的,所以可以這樣寫。
// 把 One 的 memo 移除
function One({ data }: Props) {
console.log("One render"); // 觀察元件是否 re-render
return <p>One number: {data.value}</p>;
}
// ...
// 用 useMemo 回傳 JSX
const one = useMemo(() => {
return <One data={{ value: number }} />;
}, [number]);
// ...
// 把回傳的 one 直接放在 return 裡
return (
<div>
{one}
<Two data={newCount} />
<button onClick={handleNumber}>add number</button>
<button onClick={handleCount}>add count</button>
</div>
);
上面這樣寫的結果一樣可以達到我們希望的目的,但是這樣寫的話會影響到 code 的可讀性跟維護性。
透過 useMemo 可以避免不必要的計算,提高網站的效率跟執行速度。
但是 useMemo 並不是用越多越好,如果網站在執行上沒有明顯卡頓的情形就不用使用,因為記憶跟比較也是會影需要計算成本的。
即使是上面的範例也有其他解決的方式不一定要使用 useMemo,而是應該要處理費波那契數列計算耗時的問題。
下一篇簡單介紹優化 react 的另一個方法 - useCallback
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium