(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教學!