再來是另一個很常跟 useState 搭配使用的 useEffect,負責執行side effects(副作用):
The Effect Hook lets you perform side effects in function components.
Side effect簡單來說指的是執行函式 return 以外的任務;在React裡面什麼是side effect?我們知道React的主要任務是處理畫面,所以components 回傳的是 jsx,可以看作是要呈現在畫面上的節點。
因此在React裡面,廣義地來說,任何跟呈現畫面以外的任務都可以看作是side effect,例如取得資料(data fetching)、處理訂閱事件(setting up a subscription)和手動操作DOM(manually changing DOM)等都可以算是React裡面side effects。
語法與執行時機
有關 useEffect
的語法和執行時機有幾個要注意的事項:
useEffect( () => {}, [dependencies])
- 語法:
useEffect
裡第一個參數要放一個回呼函式(callback function),回呼函式內寫下要等會執行的 side effect (可以簡稱這個回呼函式參數為 effect );- 第二個參數要放的是一個陣列(array),陣列裡面要放的是任何跟執行effect有關的dependencies,dependencies可以是state變數、props、function等(注意function是reference type,最好用
useCallback
包起來); - 第二個參數是空陣列
[]
,代表這個effect只會在component第一次render執行,之後不會再執行。
useEffect
是在component render以後才執行,所以useEffect
可以存取到最新的state。Component第一次render必定會執行
useEffect
,所以如果不想讓side effect第一次render被執行,可以加上一個init
變數判斷:import { useEffect } from 'react' let init = false; // * export default function Something(){ useEffect(() => { if( !init ) { init = true; // * return; // * } // execute side efect }, [dependencies]) // ... }
Dependencies
前面有談到 useEffect
第二個參數要放的是跟執行effect有關的dependencies。
如果是加入一個空陣列 []
則是只會在component第一次render時執行side effect,通常網頁載入後會想要呼叫API擷取一次資料,等到符合某項條件再繼續呼叫這個API要求更多資料,因此這時就會在第二個參數加入一個空陣列。
若是連空陣列都不放,則每次component render時都會執行side effect,這通常是一件很危險也很不必要的事情,所以如果不希望component更新時都會執行side effect,這就是為什麼這個時候可以在useEffect第二個參數加入dependencies,限制dependencies內的某些state、props或function改變時才執行這個effect,不過等會還是有個例子來看怎麼使用。
例如,現在component有三個state:目前時間(timer)、從API擷取的資料(data)和負責計數目前已經呼叫多少次API(counter)。
Component 刻意增加一個 timer,在每次render時都執行一次第(1)個useEffect記錄每次render的時間,但不限定要記錄什麼類型的render。
第(2)個useEffect則是用擷取API資料,因為只需要在component第一次render時執行,所以就是在第二個參數放空陣列。
第(3)個useEffect的dependencies參數則是加入 counter state [counter]
,表示每次當 counter 值改變時我們才更新 document.title
;這也代表另一個state timer 每次改變時並不會使得 document.title
有所改變。
import {useState, useEffect} from "react";
function Component() {
const [timer, setTimer] = useState(new Date().toLocalTimeString());
const [data, setData] = useState([]);
const [counter, setCounter] = useState(0);
const recordTime = ()=>{
// update timer
}
const fetchData = ()=>{
fetch(...)
.then(res=>res.json())
.then(parsedData=>{
setData(parsedData);
setCounter(counter+1);
})
}
// (1)
useEffect(()=> recordTime());
// (2)
useEffect(()=> fetchData(), []);
// (3)
useEffect(()=>{
document.title = `Counter has benn clicked {counter} times`;
}, [counter]);
}
綜合上述 useEffect
的使用方式,在實務上可以運用一些 useEffect
的性質加強程式的效能:
- 函式只使用一次或者不需要給組件(component)其他元素共用時(例如某個按鈕的onClick事件也要用到同個函式),可以將函式定義在
useEffect
內並給予空的dependencies呼叫,如此一來可避免函式在組件重新渲染時會再定義一次; - 相反地,函式要在組件其他地方共用,就要拉出到
useEffect
以外、不能定義在useEffect
內;
延伸第2點,定義在 useEffect
以外的函式會在組件重新渲染時重新要一塊記憶體位址並定義相同內容、記憶體位址不同的同個函式,若此函式要作為 useEffect
的dependencies就會因函式儲存的記憶體位址不同而呼叫 useEffect
,若要避免反覆執行 useEffect
的此狀況就要使用 useMemo
或 useCallback
來保存在同一個記憶體位址的函式物件,這個概念可參考另一篇文章。
清除side effect
useEffect
預設是在component unmount之後才清除執行後的效果。**有時候我們會希望執行effect之後可以立即清除效果,能在useEffect裡面增加 return
來清除。
例如下面程式碼隨意新增了一個瀏覽器的監聽事件,當使用者捲動網頁時會去設定subscription,不過為了獲取最新的subscription狀態,每次都要立即清除前一次subscription狀態。
import { useEffect } from "react";
function component() {
const subscriptData=()=>{
/* subscript something*/
}
useEffect(()=>{
// perform
window.addEventListener("load", subscriptData);
// clear
return ()=>{
window.removeEventListener("load", subscriptData);
};
});
}
接著彙整清除side effect的清除函式(cleanup function)的執行時機:
- Component第一次render時,清除函式不會執行;
- 每次useEffect執行前,清除函式都會執行並清除前一次執行的effect,這裡可以用
console.log
去看會發現前一次執行的effect並不會出現; - 而component ummount時也會執行一次清除函式。
最後分享一篇講的非常淺顯易懂、很棒的effect中文教學文 ─ 轻松学会 React 钩子:以 useEffect() 为例
References