【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 一些纪录
官方文件


<<:  DAY26 变数

>>:  38天-研究du用法

第一天 参赛宣言

经历了前两次的失败,决定还是第一天不要直接写文章! 换个心情,先写了参赛宣言。好好的展开一个挑战的开...

[Day5] 函式(数)介绍

1.前言 今天来讲讲函式(不是韩式料理的韩式),而是Coding时会用到的程序方法(你到底在讲啥?)...

Day07 X Image Sprites

经过昨天的一番折腾,我想读者们都对基本的图片优化稍有概念了,今天要介绍的优化技巧其实严格来说也算是...

玩通灵 - Intigriti's 0521 XSS challenge (Clickjacking)

前言 这是之前 Huli 大在前端社团分享的 国外 XSS 挑战。 最近比较有时间来分享,当时 「从...

Why Spring Boot?

大家好,我想介绍一下自己为什麽会认识spring boot,因为写後端API的时候会用到的框架 然後...