寫Vue的時候,偶爾會需要使用watch去監聽某個state的變動,來去進行一些邏輯操作。在接觸React之後,我發現React也有一個類似用途的hook,這個hook就是useEffect。不過在最近好好認識這個hook後,才發現這個hook說是跟watch很類似,卻又有一點不太ㄧ樣。今天一樣從Vue的watch開始認識React的useEffect吧!
首先,來快速看一下Vue的watch。就如同它的名字一樣,watch通常使用於監聽某些值的變動來做進一步的邏輯操作,例如我希望實作的效果是「當我點擊按鈕到數字變5時,印出console.log。」時,就可以透過watch監聽數字的改動。
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
name: 'WatchExample',
setup() {
const count = ref(0);
// 監聽 count 的變化
watch(count, (newValue, oldValue) => {
if (newValue === 5) {
console.log('count 達到 5 了!');
}
});
const increment = () => {
count.value++;
};
return {
count,
increment,
};
},
};
</script>
以上這個情境就是透過監聽count的變動,來決定何時要顯示提視窗。
另外,watch除了監聽state的變動外,還可以取得變動前及變動後的state,來進行相對應的操作。
前面快速看完watch的用法和使用情境後,再來直接看一下前面提到的情境,用useEffect的話,可以怎麼做。
useEffect在使用上會傳入兩個參數,一個是 副作用的函式
,在這個函式內還可以return一個執行清除副作用的函式
,也被成為cleanup,而另一個參數則是與這個副作用有相依的 相依值陣列
。
useEffect(() => {
// 要進行的操作
console.log('side effect');
// 清理函式
return () => {
console.log('Cleanup is happening');
};
}, [dependencies]);
所以如果要實作「當我點擊按鈕到數字變5時,印出console.log。」的情境,就會是以下這樣的寫法。
useEffect(() => {
// 要進行的操作
if (count === 5) {
console.log('count 達到 5 了!');
}
}, [count]);
單純從前面的例子來看的話,大多數的人可能都會覺得useEffect跟watch很像,甚至會覺得「useEffect根本是watch吧!」,因為useEffect和watch一樣可以會因為特定state的變動,來進行一些邏輯的處理。但useEffect實際上並不只是在監聽state的變動,真的要說的話,它反而比較像是生命週期的API,但它卻也不是所謂的生命週期API。接下來就來透過一些實際的例子,了解useEffect的特性和使用概念吧!
useEffect除了固定會在第一次渲染的時候執行外,還有一個特性是當你沒有透過「第二個參數-相依值陣列」限制它要在因為哪些相依的state有變動而執行時,它其實會在每次渲染都被執行
。
例如這樣調整的話,其實是會在每次畫面重新渲染時,都執行useEffect。
useEffect(() => {
console.log("Effect is running");
if (count === 5) {
console.log("count 達到 5 了!");
}
// 不透過第二個參數傳入相依state的陣列
});
當有傳入第二個參數,也就是dependicies陣列時,就只會當相依的值有變動時,才執行useEffect。
useEffect(() => {
console.log("Effect is running");
if (count === 5) {
console.log("count 達到 5 了!");
}
// 加上dependicies 陣列
}, [count]);
為了避免前一次的副作用造成再次執行副作用時產生問題,不只在元件umount的時候會執行cleanup,在每次重新渲染後,也會先執行上次副作用的cleanup。
useEffect(() => {
console.log('current: ', {count});
return () => {
console.log('clean up');
};
}, [count]);
useEffect的副作用函式會在畫面渲染後執行,無法以非同步的方式執行,所當寫這樣的非同步函式當作副作用函式時,就會出現warning訊息。
useEffect(async () => {
console.log('setup')
return () => {
console.log('clean up');
};
}, []);
但是總有一些時機點,需要在處理副作用的時候,透過非同步的方式執行一些邏輯,這時可以改個做法,在副作用函式裡面宣告一個非同步的函式再使用。
例如:
useEffect(() => {
const fetchData = async () => {
const response = await getData();
};
fetchData()
}, []);
看了以上特性後,應該可以感覺得到其實useEffect真的不是單純的watch,也不算是生命週期API。
現在可以再花一點時間回頭思考一下「為什麼useEffect實際上並不算監聽也不算是一個生命週期的hook」。useEffect在使用上,並不是單純地在監聽某個值,因為它在第一次渲染時,其實就會先執行一次useEffect的副作用函式,接下來再次渲染才會依照第二個參數有無帶入,或是帶入什麼相依值,而決定是否再次執行useEffect的副作用函式,並且會在umount的時候,或是下一個副作用函式執行前先進行清除前一次副作用的動作。useEffect雖然會隨著元件的生命週期執行,也會因為相依的值有變動而執行,但是它並不是用來監聽某個沒有關聯的值來處理邏輯,也不是只有在特定的生命週期才會執行
,它更像是隨著整個functional component的資料變動,下去處理相關的副作用,所以它的用途應該定位在處理副作用
,而不只是單純的監聽或生命週期API。
當我們在未使用Strict Mode的時候,第一次render時,只會執行一次useEffect的副作用,第二次render才會先清掉上次的副作用,再進行一次副作用。但是當我們在開發模式中使用Strict Mode的時候,初次渲染就會先進行副作用,再進行cleanup,最後再進行一次副作用。所以當我們第一次進入畫面時,就可以看到console會呈現這個樣子。
setup -> cleanup -> setup
這個部分也跟元件在渲染時,都會經歷mount->unmount->mount一樣,都是在避免一些預期外的問題產生。
今天了解了一個在React中,很重要的hook - useEffect,除了學習怎麼使用外,也了解了一些useEffec的特性。更重要的是知道它並不是單純的生命週期,也不是單純的watch,雖然用途或使用時機可能有點雷同,但因為它主要是在處理副作用,還是需要隨著相關state的變動下去思考它的使用時機點。雖然把它拿來監聽與副作用邏輯沒有直接關聯的state也會有作用,但是為了避免不預期的狀況出現,還是依照相依值陣列存在的用意,放上與副作用確實有相依的值會比較好。
useEffect
useEffect 其實不是 function component 的生命週期 API