React 的第六個 hook:useCallback

也是跟效能有關。

簡述

把 function 給記起來,如果 dependencies 沒有變,就不會重新宣告新的 function。

useCallback 說白話一點就是上面那樣子,它跟 useEffect 有點像,可以在第二個參數傳入 dependencies,告訴 react 只有在 dependencies 變的時候才幫我宣告新的 function,不然就沿用原本的就好。

接下來的範例會稍微複雜一點,所以先做段解說:

  1. 宣告兩個 function,一個會用 useCallback 一個不會。
  2. 利用 ref 來把一開始的 function 給記住
  3. 每次 re-render 時就跟一開始的 function 做比對

另外這邊一樣有用到 memo,所以要記得 memo 的特性是「當 props 改變時才會 re-render」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 用來檢查是否相等的 function
const checkIsEqual = (a, b) => a === b ? "相等" : "不相等";

// 用 memo 記起來的 Component
const Component = memo(props => {
const counter = useRef(0);
const message = props.isUseCallback
? "有用 useCallback"
: "沒有用 useCallback";
counter.current++;

return (
<div>
{message} 的 component 已經被 render 了 {counter.current} 次
</div>
);
});

function App() {
const [value, setValue] = useState();
// 只是個用來當作 dependencies 的 state
const [someArg, setSomeArg] = useState("argument");

// 用 useCallback,並把 dependencies 設為 someArg
const handleSomethingUseCallback = useCallback(() => {}, [someArg]);
// 沒有做額外設定
const handleSomething = () => {};

// 用 ref 把第一次的值記住,只要我們沒有自己去改值,它就不會變
const refHandleSomethingUseCallback = useRef(handleSomethingUseCallback);
const refHandleSomething = useRef(handleSomething);

// 處理 input 的 state
const handleChange = (e) => setValue(e.target.value);

return (
<div className="app">
<input type="text" value={value} onChange={handleChange} />

<div>
有用 useCallback 的比對結果:
{checkIsEqual(
refHandleSomethingUseCallback.current,
handleSomethingUseCallback
)}
</div>

<div>
沒用 useCallback 的比對結果:
{checkIsEqual(
refHandleSomething.current,
handleSomething
)}
</div>

<Component
isUseCallback={false}
handleSomething={handleSomething}
/>
<Component
isUseCallback={true}
handleSomethingUseCallback={handleSomethingUseCallback}
/>
</div>
);
}

use-callback

它的流程是這樣:

  1. 輸入文字,改變 value 的 state,觸發 re-render
  2. 沒有用 useCallback 的 function 會被重新宣告,所以 Compnonent 會被 re-render(因為 props 改變)
  3. 接著拿 useRef 存的值跟這次的 function 比對,得到 false

而有用 useCallback 的 function 因為 dependencies 沒有變(someArg),所以不會被重新宣告,也不會 re-render。而比對的部分也會是 true,因為 function 的值沒有改變。

useCallback 的用法就是這樣,不過最後要特別強調一件事情:

關於 useCallback 的 dependencies,一定要記得把 function 中有用到的依賴變數 or function 放進去,不然有可能會出現靈異現象(沒有重新宣告,所以拿不到最新的 state)

實際應用

接著用一個實際案例來示範 useCallback 會用在什麼樣的地方。

一個最常見的例子是「串接 API」,最基本的 pattern 會長這樣:

1
2
3
4
5
useEffect(() => {
fetch(...)
.then(res => res.json())
.then(json => setList(json))
}, [])

不過如果想用 async / await 的話就比較麻煩了,因為 useEffect 本身不可以直接用這個語法。第一種方式是直接宣告在裡面,像這樣:

1
2
3
4
5
6
7
8
useEffect(() => {
const getList = async () => {
const res = await fetch(...)
const json = await res.json()
setList(json)
}
getList()
}, [])

這樣子做沒什麼問題,唯一的缺點是可讀性不佳,所以我們通常會希望能宣告在外面,像這樣:

1
2
3
4
5
6
7
8
9
10
11
function App () {
const getList = async () => {
const res = await fetch(...)
const json = await res.json()
setList(json)
}

useEffect(() => {
getList()
}, [])
}

做到這邊以後,你就會發現 ESLint 很熱情地告訴你:

哈囉,你確定不要把 getList 這東西放到 dependencies 裡嗎?

沒有想太多的你就乖乖照做了,結果就陷入「無窮迴圈」了 XD

這邊我不解釋太多,但主要的原因是因為「Reference」。

簡單來說就是每一次 re-render 時都會重新宣告 getList 這個 function,而每一次的 function 都會是不一樣的 function。

如果你不太懂的話,試著想想看這個例子大概就明白了:

1
2
3
const a = () => {}
const b = () => {}
console.log(a === b) // ???

所以這時候就輪到 useCallback 出場了,我們可以改寫成這樣子:

1
2
3
4
5
6
7
8
9
10
11
function App () {
const getList = useCallback(async () => {
const res = await fetch(...)
const json = await res.json()
setList(json)
}, [])

useEffect(() => {
getList()
}, [getList])
}

附註:這邊的 getList 因為沒有用到其他相關的 variable 或 state,所以 dependencies 是空的,也就是說不會被重新宣告。但如果你在實作時有這個需求,請務必記得加上去,避免出現非預期的結果。

useCallback 在這邊的任務就是「檢查 dependencies 有沒有變?」,如果沒有,就會回傳 cache 住的 function(就是原本的那個啦),反之則重新宣告一個新的 function。

既然現在 function 不會被重新宣告,那麼就可以把它填入 useEffect 的 dependencies 中,不會再進入無限迴圈囉!

React 的第七個 hook:useMemo React 的第五個 hook:memo
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×