終究都要學 React 何不現在學呢? - React 基礎 - useEffect - (8)

前言

接下來我們要來認識一個新的 React Hook,也就是 useEffect,那麼單看這個名子會覺得有一點特別,因為直接翻譯成中文來講是「使用效果」,那麼這是什麼東西呢?我們就來認識一下吧。

Side Effect

開始 useEffect 之前我們要先來認識一個東西,也就是 Side Effect(副作用),那什麼是 Side Effect 呢?

最常見的形容詞就是醫生開的「藥」。

沒有錯,就是你想的那一個藥,醫生開給你的處方簽,你試想一下,你在感冒後拿到的藥袋上往往都會寫著「副作用」,而這個副作用可能會有過敏反應、嗜睡、腸胃不適等狀況。

雖然不得不承認「副作用」,這個中文很容易讓人覺得都是壞的,但是其實副作用也同時代表著有幫助、有益處的,讓我們來看一段簡易的描述…

1
你吃完藥之後身體會好轉,但相伴的就會是腸胃不適的反應

上面描述中,其實就包含了兩個 Side Effect,分別是「身體好轉」與「腸胃不適」,因此 Side Effect 並不全然都是壞的。

那麼為什麼要提到這個呢?其實像是我們常見的 AJAX、DOM 操作,甚至是 console.log 都是屬於 Side Effect 的一種,而我們實際開發上也時常仰賴著 Side Effect,因此了解 Side Effect 是有助於避免寫出一些危險的程式碼。

那以程式碼世界來講 Side Effect 是什麼樣的狀況呢?舉例來講,重新賦予的行為也是一種 Side Effect

1
2
3
var myName = 'Ray';
... // 幾千行之後
myName = 'Array';

相信你應該沒想到重新賦予值也算是 Side Effect 的一種吧。

沒有錯,只要你會修改到原始值就算是 Side Effect 的一種,我們可以看到上面範例的原始值是 'Ray',後來不知道幾百、幾千行之後我們將 myName 重新賦予成 'Array',而這過程就有可能會導致一些我們不可預期且難以排除的錯誤。

當然這邊只是大概了解一下而已,如果你對這一塊很感興趣的話,可以考慮參考看我先前寫的「簡單趣談 Functional Programming in JavaScript」會更詳細。

這邊只是簡單談到什麼是 Side Effect 而已,畢竟 React 官網有一段是這樣:

資料 fetch、設定 subscription、或手動改變 React component 中的 DOM 都是 side effect 的範例。

那麼大概了解什麼是 Side Effect 後,我們就準備來認識 useEffect 吧。

useEffect

useEffect 主要大多用途都是在於我們畫面 render 之後要做某些事情,其實也就是在跟 React 說等一下 render 後要記得執行裡面的某些事情,通常放在這裡面的東西大多都跟 SideEffect 有關係,因此基本上會產生 Side Effect 的行為都會建議在 useEffect 裡面,像是前面所提到的 AJAX 行為、修改 DOM 操作等。

那如果放到 Vue 中的話 useEffect 類似什麼呢?類似 Composition API 的 onMounted

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

const app = createApp({
setup() {
const text = ref('Hello Ray.');

onMounted(() => {
console.log(text.value)
})
}
});

app.mount('#app');

雖然前面有提到 useEffect 就是「等一下 render 後要記得執行裡面的某些事情」,但 useEffect 基本上是會傳入兩個參數的,分別是函式跟陣列,第二個陣列主要是選填,因此隨著你的使用方式不同而也會得到不同的結果,因此讓我們接著認識一下 useEffect 各種做法吧。

After Ever Render(每次渲染之後執行)

首先先讓我們看看第一種 useEffect 的範例程式碼

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

React.useEffect(() => {
console.log('render');
})

return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
Count is: { count }
</button>
</div>
)
}

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

useEffect 如果只傳入一個函式,而不傳入第二個參數時,當你點擊畫面按鈕後就會發現 console.log('render') 是會不停的被觸發的。

因此第一種 useEffect 代表著每次畫面只要被觸發渲染機制,那麼 useEffect 就會跟著重新執行一次。

Once(只執行一次)

可是實際上來講,我們時常只會需要執行一次而不是每一次重新渲染就觸發一次 useEffect,那這時候該怎麼辦呢?這時候只要針對 useEffect 傳入第二個參數,也就是一個空的陣列這樣子就可以達到只觸發一次的需求

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

React.useEffect(() => {
console.log('once render');
}, []);

return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
Count is: { count }
</button>
</div>
)
}

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

接下來你不管怎麼點 Count 按鈕都不會向前一個範例一樣重複觸發 useEffect,這概念其實與 Vue 的 onMounted 的生命週期神似。

On State(監聽特定值)

useEffect 的第二個參數除了可以傳入空陣列之外,也可以傳入你想要監聽的東西,那麼監聽什麼東西呢?也就是監聽 useState,當你的 useState 有任何變化時,就會觸發 useEffect

但是這邊要注意一件事情 useEffect 必定會先執行過一次,所以你在 console 會看到一個 render 是正常的,接著後續更改監聽的 useState 值時才會再次觸發。

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
function App (){
const [count, setCount] = React.useState(0);
const [myName, setMyName] = React.useState('Ray');
function sayHi () {
window.alert('Hello Ray.');
}

React.useEffect(() => {
console.log('render');
}, [ count ])

return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
Count is: { count }
</button>
<hr />
<button type="button" onClick={() => setMyName('QQ')}>
改名字 { myName }
</button>
</div>
)
}

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

我們可以看到當你點下 Count 時,useEffect 就會跟著觸發一次,但若你點 改名字 的按鈕時,則不會觸發 useEffect,透過監聽設定的值就可以避免 「After Ever Render」 狀況發生。

當然,你也可以綁定多個 State。

1
2
3
React.useEffect(() => {
console.log('render');
}, [ count, myName ])

而這概念也非常雷同於 Vue 的 watch

cleanup function(清理函式)

cleanup function 的中文叫做清理函式,這個稍微有一點特別。

那麼該怎麼撰寫 cleanup function 呢?其實很簡單,只要 useEffect 裡面有一個 return 函式,那麼這就叫做 cleanup function

1
2
3
4
5
6
useEffect(() => {
console.log('useEffect');
return () => {
console.log('cleanup function');
}
})

那這個觸發時機點在什麼時候呢?首先第一次進入畫面時會觸發 console.log('useEffect');,接下來就沒有觸發 console.log('cleanup function');,而觸發 'cleanup function 的時機只有在你觸發重新渲染畫面時才會被呼叫一次

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

React.useEffect(() => {
console.log('useEffect');
return () => {
console.log('cleanup function');
}
})

return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
Count is: { count }
</button>
</div>
)
}

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

我們可以看到一開始 console.log('useEffect'); 會被觸發,接下來當你按下 Count 按鈕後,就會先觸發 console.log('cleanup function'); 然後再觸發 console.log('useEffect');

而這個 cleanup function 就很適合用於初始化狀態或者清除特定的事件綁定,如果以 Vue 來講就類似於 beforeDestroyonBeforeUnmount 的生命週期。

透過上面知識點我們可以知道 useEffect 相較 useState 是比較複雜一點的。

useEffect 常見雷點

許多人在初學使用 useEffect 時會遇到一個雷點,也就是「監聽與賦予的值相同」,然後就導致 useEffect 直接爆掉,下方是一個爆掉範例,你可以自己打開註解試試看,但是請慎用(爆掉不負責) :D

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

// 請謹慎開啟註解,要有心理準備電腦會當機
// React.useEffect(() => {
// setCount((pre) => pre + 1);
// }, [ count ])

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

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

後記

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