(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
花了那麼多時間講生命週期,那麼function component的生命週期也是直接宣告componentDidMount那些嗎?
呃,不是。跟state
一樣,在function component中我們必須要使用React hook才能設定生命週期函數。
在前面講生命週期時,我們提到最常被使用到的是componentDidMount
、 componentWillUnmount
和componentDidUpdate
這三個函數,而React hook把這三者整合起來,變成了useEffect
。
之前沒有特別講,在這裡提一下,後面會講custom hook是什麼。
useEffect
接收兩個參數,第一個是一個函式,定義componentDidMount或componentDidUpdate要做什麼事,此函式的回傳值也要是一個函式,表示componentWillUnmount 要做什麼事。第二個是一個array,裡面是定義當哪些變數被改變時,這個useEffect要重新被觸發。有點像是過去我們在componentDidUpdate寫prevState!=this.state
這種感覺。
這樣講有點抽象,我們來看語法:
第二個參數為空array時(不是省略歐),代表除了第一次以外,接下來每次re-render時,沒有任何東西的改變可以重新觸發useEffect,所以就等同於componentDidMount。
useEffect(() => {
/* 下面是 componentDidMount */
/* 上面是 componentDidMount */
}, []);
/* 第二個參數是用來限定當哪些變數被改變時useEffect要觸發 */
componentWillUnmount就是useEffect第一個用來當參數的函式的return值。以下是componentDidMount和componentWillUnmount的集合體。
useEffect(() => {
/* 下面是 componentDidMount*/
/* 上面是 componentDidMount */
return (() => {
/* 下面是 componentWillUnmount */
/* 上面是 componentWillUnmount */
});
}, []);
/* 第二個參數是用來限定當哪些變數被改變時useEffect要觸發 */
我一開始在看useEffect的時候,以為第二個參數array不為空、不省略就是componentDidUpdate:
useEffect(() => {
}, [dependencies參數]); /* 第二個參數是用來限定當哪些變數被改變時useEffect要觸發 */
但實際上我觀察到在第二個參數array不為空下,第一次render還是會執行effect。查了一下官方文件,精確的說法是第二個參數是用來限制哪些參數在re-render時如果被改變,可以重新觸發useEffect,也就是第一次render無論如何都會執行。(「可以重新觸發useEffect」這句話用英文看會更精確,應該是recreate an effect)。
如果想要實現componentDidUpdate,應該要搭配另外一個React hook: useRef
。這裡先不解釋它,可以當作是產生一個「被改變後不會觸發re-render」變數的hook(其實我也還沒有特別去研究它XD)。也就是我們用useRef去產生一個紀錄是否完成第一次渲染的變數,初始值預設會給false,第一次執行effect後改為true。
也就是嚴格實現componentDidUpdate的語法為:
const mounted=useRef();
useEffect(()=>{
if(mounted.current===false){
mounted.current=true;
/* 下面是 componentDidMount*/
/* 上面是 componentDidMount */
}
else{
/* 下面是componentDidUpdate */
/* 上面是componentDidUpdate */
}
return (()=>{
/* 下面是 componentWillUnmount */
/* 上面是 componentWillUnmount */
})
},[dependencies參數]); /* 第二個參數是用來限定當哪些變數被改變時useEffect要觸發 */
如果有第二個非空參數就相當於過去寫的prevState!=this.state
和prevProps!=this.props
,如果省略,mount==true的if-else scope就是完全等於純componentDidUpdate。記得要引入useRef()。
當你「省略第二個用來監控的參數array」或是「該array不為空」時,它就等同於componentDidMount + componentDidUpdate的集合體。省略的話就是代表每次re-render時都會觸發useEffect,不省略不為空則代表當re-render時,如果你放在array中的值有被改變,就會觸發useEffect。
useEffect(() => {
/* 下面是 componentDidMount 和 componentDidUpdate */
/* 上面是 componentDidMount 和 componentDidUpdate */
return () => {
/* 下面是 componentWillUnmount */
/* 上面是 componentWillUnmount */
};
}, [dependencies參數]); /* 第二個參數是用來限定當哪些變數被改變時useEffect要觸發 */
下面省略了第二個參數,也是componentDidMount 和 componentDidUpdate集合體:
useEffect(() => {
/* 下面是 componentDidMount 和 componentDidUpdate */
/* 上面是 componentDidMount 和 componentDidUpdate */
return () => {
/* 下面是 componentWillUnmount */
/* 上面是 componentWillUnmount */
};
});
可以有多個useEffect存在同一function component和custom hook中,所以我們可以針對不同的變數去寫不同的useEffect
。
我們把之前的嬰兒整合一下,不過因為它太胖了,我們先把它變成function component,也就是把所有的state
改成useState
形式,class 的 member function改成function中的function。讓巨嬰變回中嬰(?)之後,再來慢慢把生命週期補上去。(這裡寫法其實可以更簡單,只是因為我覺得這樣寫比較快理解)
import React, { useState } from 'react';
const Baby=(props)=>{
/* 把state變成useState */
const [isGetData,setGetData]=useState(false);
const [Mom,setMom]=useState("");
const [isRightDad,setRightDad]=useState(false);
/* 把class 的 member function改成function中的function */
const ajaxSimulator=()=>{
setTimeout(()=>{
setGetData(true);
setMom("小美");
},3000)
}
const checkDad=()=>{
if(props.dad==="Chang")
setRightDad(true)
else
setRightDad(false)
}
if(isRightDad===false)
return(
<div>他的媽媽,是誰,干你X事</div>
);
else if(isGetData===false)
return(
<div id="msg">讀取中</div>
);
else
return(
<div id="msg">他的媽媽是{Mom}</div>
);
}
export default Baby;
App.js也順便換成function component,然後加入一個可以換爸爸的功能:
import React, { useState } from 'react';
import Baby from './Baby'
const App=()=>{
const [dad, setDad] = useState("Chang");
const [born, setBorn] = useState(true);
const changeDad=()=>{
if(dad==="Chang"){
setDad("Wang")
}
else{
setDad("Chang")
}
}
const spawnBaby=()=>{
if(born===true){
return <Baby dad={dad}/>;
}
}
return(
<div>
{spawnBaby()}
<div id="talk"></div>
<button onClick={changeDad}>換爸爸!</button>
<button onClick={()=>{setBorn(!born)}}>{(born===true)?"讓他回去肚子裡":"讓他生"}</button>
</div>
);
}
export default App;
import React, { useState, useEffect } from 'react';
當第二個參數為空array時,這個useEffect就相當於是componentDidMount
。
所以,我們現在讓Baby.js在被創造時,執行花3秒取得媽媽資訊的ajaxSimulator
:
const checkDad=()=>{
(省略)
}
useEffect(() => {
/* 下面是 componentDidMount */
ajaxSimulator();
/* 上面是 componentDidMount */
}, []);
if(isRightDad===false){
(省略)
}
因為我們在第一次設定爸爸跟更改爸爸時都要檢查爸爸是否正確,所以我們要用useEffect一起替代componentDidMount + componentDidUpdate。
現在加入一個useEffect替代componentDidMount + componentDidUpdate。在這邊,我們讓Baby.js的props中的爸爸第一次被渲染以及dad被改變時,用checkDad
去檢查爸爸正確性,以決定是否提供寶寶的媽媽資訊:
const checkDad=()=>{
(省略)
}
useEffect(() => {
ajaxSimulator();
}, []);
useEffect(() => {
/* 下面是 componentDidMount和componentDidUpdate */
checkDad();
/* 上面是 componentDidMount和componentDidUpdate */
}, [props.dad]); /* 加入監控的props.dad */
if(isRightDad===false){
(省略)
}
現在先在剛剛專為ajax的useEffect
加入喊聲爸的函式,讓我們等等可以用componentWillMount
移除。
useEffect(() => {
ajaxSimulator();
document.getElementById("talk").append('爸!')
}, []);
這樣按很多次之後就會呈現跟之前很像的一堆「爸!」狀況:
接下來我們加入componentWillUnmount
來在元件死去時移除「爸!」。在useEffect
中,第一個參數函式的return就是componentWillUnmount
,也就是第一個參數函式的return值也要是一個函式:
useEffect(() => {
ajaxSimulator();
document.getElementById("talk").append('爸!')
return(()=>{
/* 下面是 componentWillUnmount */
document.getElementById("talk").innerHTML="";
/* 上面是 componentWillUnmount */
})
}, []);
useState
和useEffect
是目前最常被使用的React hook。本系列關於React hook api只會介紹這兩個和之後會講的custom hook,其他還有useRef
、useContext
、useReducer
等,有興趣可以參考官方文件,或是其他目前幾篇以React hook為主體的系列。
下一篇是我覺得這次自己寫這系列想表達的重點-統整component間的溝通方式。
第二個參數為空array時(不是省略歐),代表除了第一次以外,接下來每次re-render時,沒有任何東西的改變可以重新觸發useEffect,所以就等同於componentDidMount。
不好意思,這句話我覺得有點怪,我的理解是「任何東西的改變都會重新觸發 useRffect」
然後關於第一次 render 時,我是理解成,[]中的 state 從無被建立,所以也算有改變,所以會觸發 useEffect。
const mounted=useRef();
這個部分,好奇是否可以直接用const [mounted, setMounted] = useState(false)
來替代?
不好意思,這句話我覺得有點怪,我的理解是「任何東西的改變都會重新觸發 useRffect」。然後關於第一次 render 時,我是理解成,[]中的 state 從無被建立,所以也算有改變,所以會觸發 useEffect。
我覺得我自己解釋這個可能會講不太清楚,但可以從這篇分析原始碼code的文章來看,應該可以找到你要的答案(你會發現第二個array為undefined
的case是有被handle的)
總之單就使用上而言,這兩個情形會觸發useEffect:
const mounted=useRef();
這個部分,好奇是否可以直接用 const [mounted, setMounted] = useState(false) 來替代?
我們來看以下程式碼:
function Layout() {
const [mount, setMount] = useState(false);
useEffect(()=>{
if(!mount){
setMount(true);
console.log("componentDidMount");
}else{
console.log("componentDidUpdate");
}
console.log("componentDidMount & componentDidUpdate ");
});
return (
<>
</>
);
}
執行結果:
你會發現componentDidMount
和componentDidUpdate
都被執行了。原因是當你呼叫setMount
的時候,React會重新檢查有沒有跟mount相關的渲染內容&hook依賴,整個流程是這樣的:
console.log("componentDidMount")
console.log("componentDidMount & componentDidUpdate ")
,第一個effect結束console.log("componentDidUpdate")
console.log("componentDidMount & componentDidUpdate ")
,第二個effect結束當然,這個情形你可以用新增第二個參數array並且不把mount加入這個array來解決。但是這就違反了useEffect的用意(希望effect中的內容只跟第二個參數的相依有關),所以React此時會跳warning。
另外,使用useRef也可以避免React再去做「檢查DOM是否要更新」這個動作。
感謝解答,一開始多做一次componentDidUpdate的事。
要來多研究一下useRef了
這邊想補充一下,是不是 componentDidMount 的那個 useEffect 需要寫在最後一個 useEffect?
因為 mounted.current=true 執行後會馬上生效,不像 state 會用舊的。
這邊想補充一下,是不是 componentDidMount 的那個 useEffect 需要寫在最後一個 useEffect?
如果你希望用單一變數去記憶的話,是的。
因為 mounted.current=true 執行後會馬上生效,不像 state 會用舊的。
更精確的說法是
更精確的說法是...
這部分有在此系列前面的文章看到,在之前都沒了解到這一點,獲益良多。
如果你希望用單一變數去記憶的話,是的。
是有想說可以用多個變數去記憶,但後來想想,component mount 也就那一次,感覺意義上是一進來要做的初始化,所以想說把 componentDidMount 統一寫。componentDidUpdate 再個別依不同依賴或功能分開寫。這樣好像比較順。
如果是我自己啦,會把所有comonentDidMount的東西塞在同一個effect裡。因為這樣別人review的時候比較好理解XD
哈了解,感謝
不好意思,請問const checkDad這段function 一定要在外面宣告嗎?
目前會報出 "React Hook useEffect has a missing dependency: 'checkDad'."
請問這個會否影響?
我是看此篇文章是說如果不是任何的地方都要使用的話,合併在useEffect中,
https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook
但想請問是不是要看function的作用是什麼,不一定放在useEffect中就是對的呢?
謝謝您
Hi,這邊的範例只是為了講解class component對照到function component時會是什麼狀況。
關於useEffect和一般function的關係,我們應該要換個方式來思考。接下來我會分比較多段來講解,最後再來解答你的問題。
「Effect」這個單字是「副作用」的意思。放在程式碼中,通常代表的是「某個改動」會造成的「連帶影響」。舉例來說,當我們希望當props.dad
被改動時,元件中的isRightDad
根據props.dad
去賦予新的值,此時應該這樣寫:
useEffect(()=> {
if(props.dad === 'Chang')
setIsRightDad(true);
else
setIsRightDad(false);
}, [props.dad]);
在閱讀程式語意時,我們可以很明確的知道這一段是props.dad
的副作用。
()=> {
if(props.dad === 'Chang')
setIsRightDad(true);
else
setIsRightDad(false);
}
同時,React也會希望在「副作用內容」使用到的所有可能會改變的React變數,都要被定義在useEffect的第二個dep參數array中。一方面是因為當副作用中使用到的變數被改變時,副作用的定義內容被改變,那就等於這個副作用也是「該變數的副作用」了。另一方面是這樣可以讓React避免運作時出現未預期的錯誤。
所以到這邊我們可以猜測,範例的狀況會噴錯,是因為checkDad直接使用了props.dad,所以checkDad這個函式會重新定義但又沒被放進dep array,React偵測到了可能發生的未預期錯誤。(實際上不完全是這樣,下一part我們會解釋)
但是這樣還是沒有解決我們的問題。你會發現如果我們想辦法把定義在useEffect外的checkDad改成不是直接使用props.dad,而是把props.dad透過參數傳進來,理論上函式本身定義內容應該是不變的。但是useEffect還是噴錯了:
import React, { useState } from 'react';
const Baby=(props)=>{
/* 把state變成useState */
const [isGetData,setGetData]=useState(false);
const [Mom,setMom]=useState("");
const [isRightDad,setIsRightDad]=useState(false);
const checkDad=(dad)=>{
if(dad==="Chang")
setRightDad(true)
else
setRightDad(false)
}
useEffect(()=> {
checkDad(props.dad);
}, [props.dad]);
return <></>;
}
這是為什麼呢?
React的function component和class component不同的地方在,class component在更新元件時,只會去呼叫對應的生命週期函式。但是function component在更新元件時是重新呼叫整個function component函式定義域。
舉例來說,你會發現下方程式碼中,每次按下按鍵時,"hello world!"
都被重新印出了一次:
import { useState } from 'react';
function Test(){
const [ctn, setCtn] = useState(0);
console.log("hello world!");
return (
<button onClick={()=>setCtn(ctn+1)}>
+
</button>
);
}
回到剛剛的範例,當Baby這個元件被更新時,實際上「定義checkDad」的這幾行程式碼又會被重新執行,也代表checkDad重新被定義。對於Javascript來說,checkDad就是被改變了(這段如果有疑惑可以搜尋call by reference)。
const Baby=(props)=>{
const checkDad = (dad) => {
if(dad==="Chang")
setRightDad(true)
else
setRightDad(false)
}
}
當專案規模大的時候,這會造成相當多不必要的效能問題。所以這種情形要盡量避免。
到這邊我知道問題還沒解決,但是目前為止我們可以知道兩件事:
此時我們應該把函式定義在function component定義域的 「外面」 。因為這樣是一般原生js用法,也就是只會在初始執行js檔時定義一次。
import { useState, useEffect } from 'react';
function callHelloWorld(){
console.log("hello world!");
}
function Test(){
const [ctn, setCtn] = useState(0);
useEffect(()=>{
callHelloWorld()
}, []);
return (
<button onClick={()=>setCtn(ctn+1)}>
+
</button>
);
}
這也是我們範例中的狀況(在props.dad改變後修改isRightDad,但跟isRightDad原始值無關)。
處理方式就跟你查到的stackoverflow說法一樣,透過useEffect處理即可,通常不需要多拉一個函式(當然還是要看每個case,不同情況可能有不同適合的方式),詳情可以參考我今年8月撰寫的文章的part.4 「如何正確的取得setState後的新state值?」: 理解React的setState到底是同步還是非同步(下),做法是類似的。
這個時候我們要使用React提供的原生hook useCallback
(但是在useEffect中使用該函式依然要把函式加在dep內)。請參考我12屆鐵人賽文章 【Day.20】React效能 - 用useCallback避免函式的重新定義
另外以下的狀況React並不會噴錯。這是因為React有特別設計讓useState所回傳的setState函式的reference不會改變,所以這個情形useEffect不會發生問題。
import { useState, useEffect } from 'react';
function Test(){
const [ctn, setCtn] = useState(0);
useEffect(()=>{
setCtn(1);
}, []);
return (
<button onClick={()=>setCtn(ctn+1)}>
+
</button>
);
}
以上希望能幫助到你。使用FC的思維會跟使用class有點不太一樣,現在回過頭來看覺得兩三年前自己寫的文章有些寫不是很好的地方,不好意思
喔
千萬不要這麼說,對於我這個剛學React的新手來說,我覺得您的文章對我來說是非常好的呢,哈,謝謝您,我懂了,會努力加緊學習,到時候來看進階版的React教學!