(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在前面的文章中,我們曾經說過這句話:
...如果有仔細看剛剛範例中的程式碼,你會發現Menu開關的功能並沒有被加進去。這是因為在React元件中,以內部控制元件必須要用特殊的API。如果你直接用前面的原生JS code去實作,會發現有一些問題、或是不照你的預期。
正確來說,是如果我們在React中直接使用原生DOM API,會有一些問題。這是因為當state
被改變的時候,React並不會馬上去改變DOM。
那 React 這個時候到底在幹嘛 ?
...去比較「模擬好未來長怎樣的虛擬DOM」和「當前DOM」所有節點的差別。最後,React就只會去修改「有不一樣的地方」,達到避免資源浪費的效果。
還記得我們在Day.09講過的這句話嗎? 更明確來說,React從建立、到更新畫面的流程是這樣的:
因為第一次呼叫function component時,因為元素都還沒有建立到DOM上,所以你如果直接在function component定義域操作DOM,會操作不到東西。
但是這樣我們到底要怎麼才能夠在操作到DOM呢?
為了解決這個問題,React在剛剛的流程中,插入了幾個階段:
side Effect,中文稱作副作用。在程式中指的是當前操作的對象會不會牽連到其他地方。
useEffect也是一個函式、一個React hook,他接收兩個參數,第一個參數是個函式,第二個參數是個array。useEffect第二個array指的是「當哪些state和props被設定時」要觸發副作用。
也因此,當我們讓useEffect的第二個參數array留空,就能只在建立元件後操作DOM。避免重複操作或是操作不到元素
import React, {useEffect} from 'react';
那我們要怎麼定義副作用的內容呢? 如果在剛剛的階段4和10與第二個array中的東西有關,React會呼叫第一個參數函式,我們可以把「state和props被設定時要做的事情」定義在這個函式中。又因為副作用一般會包含初始化後的影響,所以任何useEffect都會在階段4被呼叫。
比較特別的是第一個參數函式的「回傳值」也是一個函式,這個回傳函式只會在上述的階段13呼叫。
useEffect(()=>{
/* 建立 and 更新元件的副作用區 */
/*---------------------------*/
return ()=>{
/* 移除元件的副作用區 */
/*---------------------------*/
};
},[]); /* 用來限制副作用要以哪些state和props作為觸發條件的array */
又因為第二個參數是在限制哪些state和props會觸發副作用,所以如果你給了一個空array,就代表只有在第一次渲染後(階段4)會觸發副作用。後面元件更新都不會觸發。
相反的,如果你第二個參數省略不給,React會認為這代表這個副作用不需要被限制,所以除了「第一次更新DOM後」,之後元件中每個state和props被改變時都會觸發這個副作用。
這裡就不逐一講解了,對照一下前幾天的程式碼就會知道在幹嘛。基本上就是設定當isOpen
被改變時,我們要做什麼事情。
// 引入useEffect
import React, {useState, useEffect} from 'react';
/* 省略上半 */
function Menu({ title, children }){
const [isOpen,setIsOpen] = useState(false);
useEffect(()=>{
if(isOpen){
document.getElementsByClassName("menu-btn")[0].textContent = "^";
document.getElementsByClassName("menu")[0].style.display = "block";
} else {
document.getElementsByClassName("menu-btn")[0].textContent = "V";
document.getElementsByClassName("menu")[0].style.display = "none";
}
},[isOpen]);
return (
<div style={menuContainerStyle}>
<p style={menuTitleStyle}>{title}</p>
<button className="menu-btn" style={menuBtnStyle} onClick={()=>{setIsOpen(!isOpen)}}>
V
</button>
<ul className="menu">{children}</ul>
</div>
);
}
export default Menu;
請注意如果當你呼叫setState時,React並不會馬上改變state,這件事衍伸問題是,當你有多個useEffect,並且在其中一個useEffect中呼叫setState時,後面的useEffect並不會馬上拿到新的state,React會等該update流程所需要執行的所有useEffect都執行完才會更新state
const [mount, setMount] = useState(false);
useEffect(()=>{
setMount(true);
},[]);
useEffect(()=>{
console.log(mount)
//印出false
},[]);
這是因為React要確保在該次更新流程中,還沒被執行的useEffect不會因為前面useEffect呼叫了setState而導致本來「還沒被執行的useEffect的副作用」以非預期方式運作。
如果你想要取得更新後的state值,應該要新增「專門處理該state」的useEffect
const [mount, setMount] = useState(false);
useEffect(()=>{
setMount(true);
},[]);
useEffect(()=>{
console.log(mount)
// 先在元件建立時印出false,修改完mount後再印出true
},[mount]);
useEffect一般會在以下情景使用
上面已經解釋了。
為了避免重複監聽,所以只在建立元件後(階段4)add,並在移除元件前(階段12)remove。
為了避免重複Interval,所以只在建立元件後(階段4)add,並在移除元件前(階段12)remove。
避免React渲染方式和外部函式庫運作有衝突。
因為通常只會呼叫一次,所以最常在建立元件的時候呼叫
useEffect的前身是class component的生命週期。class component的生命週期有更多可以控制效能的函數,不過一般也只會用到「建立後」、「更新後」、「移除前」。有興趣的可以參考我去年的參賽文章,或是這個網站。