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

前言

這一章節開始我們要來介紹一個新的 Hook 也就是 useMemo,但是再說明 useMemo 之前我們先來回顧一下 Vue 裡面與 useMemo 類似的東西,也就是 Vue Computed。

Vue Computed

首先讓我們先回憶一下關於 Vue Computed 的用法,我們先來看一下下面這個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { createApp, ref, computed } = Vue;

const app = createApp({
setup() {
const num1 = ref(2);
const total = computed(() => num1.value * 2)

return {
total
}
}
});

app.mount('#app');

在上方的例子中我們可以看到我們所熟悉的 Computed (計算屬性),而這個計算屬性會回傳 num1 乘以 2 的值,而這就是我們所熟悉的 Composition API Computed 計算屬性最基本的用法。

忘了 Computed 的用法嗎?沒關係,這邊寫了一個簡單的範例讓你回憶一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { createApp, ref, computed } = Vue;

const app = createApp({
setup() {
const num1 = ref(2);
const total = computed(() => {
console.log('computed');
return num1.value * 2
})

const add = () => {
console.log('methods');
return num1.value * 2
}

return {
total,
add
}
}
});

app.mount('#app');

當你打開 Console 的時候,你可以發現 Add 函式總共被呼叫了五次,因此 console.log('methods') 也出現五次,而這是因為我們在畫面上呼叫它了五次,但是 Computed 明明也是呼叫五次,但 console.log('computed') 卻只出現一次,這是因為 Computed 只會在資料改變時才會重新計算並執行,因此當資料沒有任何變化時,就不會再次執行,而這也就是我們所熟悉的 Computed 的特性。

那麼 React 的 useMemo 又是怎樣呢?讓我們接著往下看吧。

useMemo

看到前面的例子後,我們可以知道 Vue 的 Computed 會在資料有變動時才會重新計算,而這個特性就是我們在 React Hook 中的 useMemo

首先讓我們將前面 Vue 的範例稍微改成 React Hook 版本,看一下 useMemo 是不是真的與 Vue Computed 類似

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
const App = () => {
const num = 1;


const total = React.useMemo(() => {
console.log('useMemo')
return num * 2
});

const add = () => {
console.log('methods');
return num * 2;
}

return (
<div>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>

<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

以結果來講,確實是一樣的,因此我們可以知道 useMemo 也是在資料有變動時才會重新計算,而這也是我們在 React Hook 中的 useMemo 的特性。

所以我們就到這邊結束囉~

.
..

….
…..

沒有啦,其實 useMemo 還有其他特性,所以讓我們繼續往下看吧。

首先先讓我們看一段範例程式碼

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 App = () => {
const [ users, setUsers ] = React.useState([]);

const getData = async () => {
// 有時候會發生 CORS,只需要重新整理即可
const { data } = await axios.get('https://randomuser.me/api/?results=10');
setUsers(data.results);
}

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

return (
<div>
<ul>
{
users.map((user) => (
<li key={user.email}>
{ user.name.first + user.name.last }
</li>
))
}
</ul>
</div>
)
}

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

這個範例程式碼我們會使用到 RandomUser 這個服務,這個服務在練習跟需要一些假的使用者資料時非常好用,完全不用擔心裡面有任何真人個資唷。

那麼 useMemo 除了與 Vue 的 Computed 神似之外,它還有什麼特性呢?首先 useMemo 其實可以傳入兩個參數,分別是 Callback 與 Array,而 useMemo 會去記憶 Callback 計算後的結果,也就是 memoized 的概念,甚至你也可以在第二個陣列中傳入想要監聽的變數,當監聽的變數有變化時就會出發渲染

1
2
const [ users, setUsers ] = React.useState([]);
const newUsers = React.useMemo(() => {}, [ users ])

但是如果你沒有傳入第二個陣列的話,則會在每一次觸發渲染時就執行一次 useMemo,如同前面範例一樣。

接下來讓我們實際撰寫一下會更有感覺,首先前面有提到 RandomUser 這個服務,在我們透過 AJAX 請求資料之後,我們會需要使用 sort 重新排序,所以你有可能這樣撰寫

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
const App = () => {
const [ users, setUsers ] = React.useState([]);;
const [ state, setState ] = React.useState(false);

const getUsers = async() => {
const { data } = await axios.get('https://randomuser.me/api/?results=10');
setUsers(data.results);
}

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

const filterUsers = (state) => {
const newUser = users.sort((a, b) => {
if(state) {
return a.dob.age < b.dob.age ? 1 : -1
}
return a.dob.age > b.dob.age ? 1 : -1
});

setUsers([...newUser]);
}

return (
<div>
<button onClick={ () => filterUsers(true) } className="border-4 border-indigo-500">年齡大到小</button>
<button onClick={ () => filterUsers(false) }className="border-4 border-indigo-500 ml-4">年齡小到大</button>
<hr className="my-4"/>
<ul>
{
users.map((user) =>(
<li key={ user.email }>
{ `Name:${user.name.title}.${user.name.first } ${user.name.last}, Age:${user.dob.age}` }
</li>
))
}
</ul>
</div>
)
}

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

雖然畫面是有正常顯示且排序,但這樣做其實並不是一件好事,因為我們每次點擊按鈕都會觸發 filterUsers,而 filterUsers 會去重新排序,這樣就會造成效能上的浪費,因為我們只是想要重新排序,而不是重新渲染,所以我們可以使用 useMemo 來優化這個問題

這邊也題外話一下,你可能會注意到 filterUsers 中的 setUsers 我是重新做陣列展開的方式 setUsers([...newUser]);,這邊會這樣寫的原因是因爲 sort 會直接修改原本的陣列,因此如果你是直接 setUsers(newUser); 的話,你會發現畫面並不會重新渲染,因為我們並沒有修改資料,所以 React 在認知上就不會重新渲染,所以我們必須要做陣列展開的方式,讓 React 認知到我們有修改資料,才會重新渲染。

接著讓我們來看一下 useMemo 這個 Hook,那該怎麼寫,其實很簡單就把它當作在寫 Vue 的 computed 就好,只是我們會特別監聽 state 這個值有變化時才去觸發 useMemo

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
const App = () => {
const [ users, setUsers ] = React.useState([]);;
const [ state, setState ] = React.useState(false);

const getUsers = async() => {
const { data } = await axios.get('https://randomuser.me/api/?results=10');
setUsers(data.results);
}

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

const filterUsers = React.useMemo(() => {
return users.sort((a, b) => {
if(state) {
return a.dob.age < b.dob.age ? 1 : -1
}
return a.dob.age > b.dob.age ? 1 : -1
});
}, [ state ])

return (
<div>
<button onClick={ () => setState(true) } className="border-4 border-indigo-500">年齡大到小</button>
<button onClick={ () => setState(false) }className="border-4 border-indigo-500 ml-4">年齡小到大</button>
<hr className="my-4"/>
<ul>
{
filterUsers.map((user, index) => (
<li key={ user.email }>
{ `Name:${user.name.title}.${user.name.first } ${user.name.last}, Age:${user.dob.age}` }
</li>
))
}
</ul>
</div>
)
}

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

以上就是一個簡單粗略的 useMemo 介紹與使用。

後記

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