iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 26
1
Modern Web

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

【Day.26】React進階 - useEffect v.s useLayoutEffect

如果你在撰寫React專案時,有試著在第一次渲染後,透過useEffect以state修改綁定給元件的資料,應該會發現一個特殊的現象:

嗯?為甚麼我的元件會閃一下?

以下方的程式為例

  • src/page/MenuPage.js
import React, {useState,useMemo,useEffect} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording = new Array(20);
menuItemWording.fill("沒有東西");

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    useEffect(()=>{
        setMenuItemData([
            "Like的發問",
            "Like的回答",
            "Like的文章",
            "Like的留言"
        ]);
    },[])

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

你會得到下列的結果:

這是為什麼呢?

非同步、畫面渲染後執行的useEffect

這是因為我們先前說過,useEffect的執行時間點是這樣:

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

也就是在第一次渲染時,React還沒有拿到你在useEffect中修改的state,所以畫面上顯示的會是你一開始拿來當初始值的資料。這也是我們的畫面會閃一下的原因。

(2020/10/11) 但如果你是從鐵人賽一開始就在跟我文章的人,我當初這裡有不小心寫錯。現在已經修正了。雖然這種人應該不多就是(?)

同步、畫面渲染前執行的useLayoutEffect

雖然這種需求很少,但React提供了一個解決上述問題的hook-useLayoutEffect。它和useEffect的語法、使用上一模一樣。唯一的差別是useLayoutEffect被提升到了渲染畫面前、更新DOM後執行。

useLayoutEffect的執行時間點是這樣:

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

接下來你可以試著把剛剛的useEffect換成useLayoutEffect

  • src/page/MenuPage.js
import React, {useState,useMemo,useLayoutEffect} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording = new Array(20);
menuItemWording.fill("沒有東西");

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    useLayoutEffect(()=>{
        setMenuItemData([
            "Like的發問",
            "Like的回答",
            "Like的文章",
            "Like的留言"
        ]);
    },[])

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

你會發現畫面不再會閃過一次初始的資料了。這是因為React在第一次渲染畫面前已經執行了useLayoutEffect中的setState

然而必須要注意的事情是,useLayoutEffect本身是一個同步函式,也就是說UI會等useLayoutEffect中做的事情結束才會渲染。所以不要在useLayoutEffect做太多事情,否則使用者看到UI的間隔會拉長,導致UX變差。

這件事衍伸的問題是,當你要在React做SSR時,因為useLayoutEffectusetEffect都不會在server-side執行,有需要useLayoutEffect的元件就可能會以不符你的預期的方式運作。

請記得,除非你有特殊的需求,否則大部份的狀況useEffect都應該能夠解決你的問題

如果你是從class component切換過來的人,實質上useLayoutEffect的執行時機點才是真正等於componentDidMount和componentDidUpdate。 但使用上官方還是希望你使用useEffect。

參考資料:
React Hooks 一些紀錄
官方文件


上一篇
【Day.25】React進階 - Custom hook | 把React component API模組化吧!
下一篇
【Day.27】React進階 - 用useReducer定義state的更動原則
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言