終究都要學 React 何不現在學呢? - React 進階 - useCallback - (14)

前言

接下來這一篇算是簡單的一篇,因為我們只會快速認識一下 useCallback,而 useCallback 是什麼呢?就讓我們繼續往下看吧。

useCallback

useCallback 其實與前面 useMemo 非常相似,甚至可以說是 useMemo 的變體也不為過,但…這樣講很模糊,到底 useMemouseCallback 有何差異呢?

簡單來講這兩個 Hook 都是為了優化 React 渲染效能而誕生的,只是這兩者各自重點皆不同

  • useMemo
    • 回傳記憶的
  • useCallback
    • 回傳記憶的函式

記憶是什麼呢?講記憶的感覺好像非常可怕,因此所以你也可以把它理解成「緩存」、「暫存」的概念,所以將上面稍微修正一下就變成以下

  • useMemo
    • 回傳記憶、暫存或緩存的
  • useCallback
    • 回傳記憶、暫存或緩存的函式

那麼 useMemouseCallback 有何差異呢?接著就讓我們來看一下範例。

請注意,以下寫法僅僅只是示範,實務上並不建議你這樣做

useMemouseCallback 都是接受一個函式與陣列,並且都是一個表達式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const App = () => {
const exampleMemo = React.useMemo(() => 1 + 1, []);
const exampleCallback = React.useCallback(() => 1 + 1, []);

React.useEffect(() => {
console.log('useMemo', exampleMemo);
console.log('useCallback', exampleCallback);
})


return (
<div>
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

但是以結果來講,兩者輸出的都是不同的東西,分別是 2() => 1 + 1,就如同前面所說的 useMemo 是回傳記憶的值,而 useCallback 是回傳記憶的函式。

那麼這時候這時候問題來了,useCallback 比較常見於哪裡呢?這讓我們先來看一個簡單範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const App = () => {
const [count, setCount] = React.useState(0);

const getData = () => {
setTimeout(() => {
setCount((pre) => pre + 1)
}, 2000);
}

React.useEffect(() => {
getData()
}, []);

return (
<div>
{ count }
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

在上方的範例程式碼中,我們使用 setTimeout 模擬 AJAX 的行為,約兩秒後會針對 count 進行加 1 的動作,然而畫面也會有所更新,這邊基本上都是沒有什麼問題的,並且只會執行一次。

但這時候若我們稍微調整一下,拆出一個元件變成以下

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
const Child = ({ count, getData }) => {
React.useEffect(() => {
getData()
}, [ getData ]);

return (
<div>
{count}
</div>
)
}

const App = () => {
const [count, setCount] = React.useState(0);

const getData = () => {
setTimeout(() => {
setCount((pre) => pre + 1)
}, 2000);
}

return (
<div>
<Child count={count} getData={getData} />
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

這時候將範例改成以上之後,有趣的事情發生了,setTimeout 會不停的重複執行,這是為什麼呢?

那麼我們簡單的分析一下上面程式碼的運作流程

  1. App 元件被渲染,getDatacount 也被傳遞給 Child 元件
  2. Child 元件被渲染,並且使用 useEffect 進行監聽,當 getData 被改變時,就會執行 getData 函式
  3. getData 函式被執行,並且進行 setTimeout 的動作,約兩秒後會執行 setCount 進行 count 的更新

到目前為止上方都還沒有什麼問題,但核心重點在於「約兩秒後會執行 setCount 進行 count 的更新」這一段

  1. 因為 count 被更新,因此觸發 re-render App 元件
  2. App 元件因為被重新渲染,因此 getDatacount 也跟著被重新生成並且傳遞給 Child 元件
  3. ChilduseEffect 發現 getData 被重新生成,因此會再次執行 getData 函式

就這樣子發生了無限循環事件。

那麼我們該如何解決呢?這時候就是 useCallback 表演的時候到了,將程式碼調整成以下

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
const Child = ({ count, getData }) => {
React.useEffect(() => {
getData()
}, [ getData ]);

return (
<div>
{count}
</div>
)
}
const App = () => {
const [count, setCount] = React.useState(0);

const getData = React.useCallback(() => {
setTimeout(() => {
setCount((pre) => pre + 1)
}, 2000);
}, [])

return (
<div>
<Child count={count} getData={getData} />
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

此時你就可以發現不會一直觸發 setTimeout 了,因為 getData 被包裝成 useCallback 之後,getData 的記憶體位置就不會被重新生成,因此 useEffect 也就不會再次執行 getData 函式。

那麼上方就是一個簡單的 useCallback 說明與範例哩~

後記

本文將會同步更新到我的部落格