iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
2
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 13

【Day.13】React入門 - useEffect(生命週期)

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


在前面的文章中,我們曾經說過這句話:

...如果有仔細看剛剛範例中的程式碼,你會發現Menu開關的功能並沒有被加進去。這是因為在React元件中,以內部控制元件必須要用特殊的API。如果你直接用前面的原生JS code去實作,會發現有一些問題、或是不照你的預期。

正確來說,是如果我們在React中直接使用原生DOM API,會有一些問題。這是因為當state被改變的時候,React並不會馬上去改變DOM。

那 React 這個時候到底在幹嘛 ?

...去比較「模擬好未來長怎樣的虛擬DOM」和「當前DOM」所有節點的差別。最後,React就只會去修改「有不一樣的地方」,達到避免資源浪費的效果。

還記得我們在Day.09講過的這句話嗎? 更明確來說,React從建立、到更新畫面的流程是這樣的:

  1. 建立、呼叫function component
  2. 真正更新DOM
  3. 渲染畫面
  4. 「某個時間點」,偵測到state、props被改變
  5. 重新呼叫function component
  6. 在virtual DOM比較所有和原始DOM不一樣的地方
  7. 真正更新DOM
  8. 渲染畫面
  9. 如果有修改state或props,則再重回流程4。
  10. 「某個時間點」,元件被移除

因為第一次呼叫function component時,因為元素都還沒有建立到DOM上,所以你如果直接在function component定義域操作DOM,會操作不到東西。

但是這樣我們到底要怎麼才能夠在操作到DOM呢?

為了解決這個問題,React在剛剛的流程中,插入了幾個階段:

  1. 建立、呼叫function component
  2. 真正更新DOM
  3. 渲染畫面
  4. 呼叫特殊函式(useEffect)
  5. 「某個時間點」,偵測到state、props被改變
  6. 重新呼叫function component
  7. 在virtual DOM比較所有和原始DOM不一樣的地方
  8. 真正更新DOM
  9. 渲染畫面
  10. 呼叫特殊函式(useEffect)
  11. 如果有修改state或props,則再重回流程5。
  12. 「某個時間點」,元件即將被移除
  13. 呼叫特殊函式(useEffect)
  14. 元件被移除

useEffect - 副作用控制

side Effect,中文稱作副作用。在程式中指的是當前操作的對象會不會牽連到其他地方。

useEffect也是一個函式、一個React hook,他接收兩個參數,第一個參數是個函式,第二個參數是個array。useEffect第二個array指的是「當哪些state和props被設定時」要觸發副作用。

另外,建立元件後(第一次渲染後),所有的useEffect也都會被觸發。

也因此,當我們讓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被改變時都會觸發這個副作用。

把前面的程式碼加入useEffect,改成用DOM api操作元素吧!

這裡就不逐一講解了,對照一下前幾天的程式碼就會知道在幹嘛。基本上就是設定當isOpen被改變時,我們要做什麼事情。

  • src/component/Menu.js
// 引入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;

重點: useEffect與setState使用關係

請注意如果當你呼叫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的應用

useEffect一般會在以下情景使用

  • 操作DOM、動畫

    上面已經解釋了。

  • addEventListenser、removeEventListenser

    為了避免重複監聽,所以只在建立元件後(階段4)add,並在移除元件前(階段12)remove。

  • setInterval、clearInterval

    為了避免重複Interval,所以只在建立元件後(階段4)add,並在移除元件前(階段12)remove。

  • 使用外部函式庫

    避免React渲染方式和外部函式庫運作有衝突。

  • 向後端呼叫api

    因為通常只會呼叫一次,所以最常在建立元件的時候呼叫

使用useEffect要注意的事情

  • 任何useEffect都會在建立元件後(階段4)被呼叫。
  • React會希望useEffect的第一個參數中有使用到的state和props都有在第二個array中,明確指出副作用與誰有關。如果沒加,React會跳warning。
  • useEffect為非同步的,React不會等它做完才去渲染畫面。(主要是向後端呼叫api時要注意)。
  • 「向後端呼叫api」這件事情,官方希望未來某天改用Suspense這個API來做,詳請情參考官方說明

更詳細的生命週期

useEffect的前身是class component的生命週期。class component的生命週期有更多可以控制效能的函數,不過一般也只會用到「建立後」、「更新後」、「移除前」。有興趣的可以參考我去年的參賽文章,或是這個網站


上一篇
【Day.12】React入門 - useState與解構賦值後的props
下一篇
【Day.14】React入門 - 輸入元素與控制組件
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言