Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

hook:useLayoutEffect

  • 用法与 useEffect 完全相同,但执行时机不同
  • 核心区别: useLayoutEffect 在 DOM 变更后、浏览器绘制之前同步执行,而 useEffect 在绘制之后异步执行

执行时机对比

渲染流程:
1. React 更新 DOM
2. 🔵 useLayoutEffect 执行(同步,阻塞渲染)
3. 浏览器绘制屏幕
4. 🟢 useEffect 执行(异步,不阻塞渲染)
Hook执行时机是否阻塞渲染用户是否看到闪烁
useLayoutEffectDOM 更新后,绘制前✅ 阻塞❌ 不会
useEffectDOM 更新后,绘制后❌ 不阻塞✅ 可能会

场景代码示例

  • 【场景 1】测量 DOM 尺寸(常用):

    • 需要在渲染前获取 DOM 尺寸,避免闪烁

      ❌ 使用 useEffect - 会闪烁

      import { useState, useEffect, useRef } from "react";
      
      const Tooltip = ({ text, children }) => {
        const [tooltipWidth, setTooltipWidth] = useState(0);
        const tooltipRef = useRef();
      
        // ❌ useEffect:绘制后执行,用户会看到位置跳动
        useEffect(() => {
          const width = tooltipRef.current.offsetWidth;
          setTooltipWidth(width);
        }, []);
      
        return (
          <div>
            {children}
            <div
              ref={tooltipRef}
              style={{
                position: "absolute",
                left: tooltipWidth > 100 ? "-50px" : "0px", // 位置会跳动
              }}
            >
              {text}
            </div>
          </div>
        );
      };
      

      ✅ 使用 useLayoutEffect - 不会闪烁

      import { useState, useLayoutEffect, useRef } from "react";
      
      const Tooltip = ({ text, children }) => {
        const [tooltipWidth, setTooltipWidth] = useState(0);
        const tooltipRef = useRef();
      
        // ✅ useLayoutEffect:绘制前执行,用户看到的是最终位置
        useLayoutEffect(() => {
          const width = tooltipRef.current.offsetWidth;
          setTooltipWidth(width);
        }, []);
      
        return (
          <div>
            {children}
            <div
              ref={tooltipRef}
              style={{
                position: "absolute",
                left: tooltipWidth > 100 ? "-50px" : "0px", // 位置不会跳动
              }}
            >
              {text}
            </div>
          </div>
        );
      };
      
  • 【场景 2】操作 DOM 避免闪烁(常用):

    • 需要在渲染前修改 DOM,避免视觉闪烁

      import { useLayoutEffect, useRef } from "react";
      
      const ScrollToTop = ({ children }) => {
        const containerRef = useRef();
      
        // ✅ useLayoutEffect:在绘制前滚动到顶部,用户看不到滚动过程
        useLayoutEffect(() => {
          containerRef.current.scrollTop = 0;
        }, [children]); // children 变化时滚动到顶部
      
        return (
          <div ref={containerRef} style={{ height: "500px", overflow: "auto" }}>
            {children}
          </div>
        );
      };
      

      对比 useEffect:

      // ❌ useEffect:用户会看到先滚动到底部,再跳回顶部的闪烁
      useEffect(() => {
        containerRef.current.scrollTop = 0;
      }, [children]);
      
  • 【场景 3】同步动画状态(常用):

    • 需要在渲染前同步动画相关的状态

      import { useState, useLayoutEffect, useRef } from "react";
      
      const AnimatedBox = ({ isOpen }) => {
        const [height, setHeight] = useState(0);
        const contentRef = useRef();
      
        // ✅ useLayoutEffect:在绘制前计算高度,动画流畅
        useLayoutEffect(() => {
          if (isOpen) {
            const contentHeight = contentRef.current.scrollHeight;
            setHeight(contentHeight);
          } else {
            setHeight(0);
          }
        }, [isOpen]);
      
        return (
          <div
            style={{
              height: `${height}px`,
              overflow: "hidden",
              transition: "height 0.3s ease",
            }}
          >
            <div ref={contentRef}>
              <p>这是一段内容</p>
              <p>可以折叠和展开</p>
            </div>
          </div>
        );
      };
      
  • 【场景 4】读取布局信息后更新(常用):

    • 基于 DOM 布局信息更新状态

      import { useState, useLayoutEffect, useRef } from "react";
      
      const ResponsiveComponent = () => {
        const [isMobile, setIsMobile] = useState(false);
        const containerRef = useRef();
      
        // ✅ useLayoutEffect:在绘制前判断是否移动端,避免布局跳动
        useLayoutEffect(() => {
          const updateLayout = () => {
            const width = containerRef.current.offsetWidth;
            setIsMobile(width < 768);
          };
      
          updateLayout();
          window.addEventListener("resize", updateLayout);
          return () => window.removeEventListener("resize", updateLayout);
        }, []);
      
        return (
          <div ref={containerRef}>
            {isMobile ? <MobileView /> : <DesktopView />}
          </div>
        );
      };
      
  • 【场景 5】第三方库需要同步初始化(进阶):

    • 某些第三方库需要在 DOM 完全准备好后立即初始化

      import { useLayoutEffect, useRef } from "react";
      import Chart from "chart.js"; // 假设是图表库
      
      const ChartComponent = ({ data }) => {
        const canvasRef = useRef();
        const chartRef = useRef();
      
        // ✅ useLayoutEffect:确保图表在绘制前完成初始化
        useLayoutEffect(() => {
          const ctx = canvasRef.current.getContext("2d");
          chartRef.current = new Chart(ctx, {
            type: "bar",
            data: data,
          });
      
          return () => {
            chartRef.current.destroy();
          };
        }, [data]);
      
        return <canvas ref={canvasRef} />;
      };
      

useLayoutEffect vs useEffect 详细对比

特性useLayoutEffectuseEffect
执行时机DOM 更新后,浏览器绘制前浏览器绘制后
是否同步✅ 同步执行❌ 异步执行
是否阻塞渲染✅ 阻塞浏览器绘制❌ 不阻塞
用户体验无闪烁,但可能卡顿可能闪烁,但不卡顿
适用场景需要读取布局、修改 DOM 避免闪烁大部分副作用(网络请求、订阅等)
性能较差(阻塞渲染)较好(不阻塞渲染)
推荐使用频率少用常用
SSR 兼容性⚠️ 服务端会警告✅ 无问题

何时使用 useLayoutEffect

情况说明示例
需要测量 DOM 尺寸获取元素宽高、位置等布局信息Tooltip 定位、响应式布局
需要在渲染前修改 DOM避免用户看到中间状态滚动位置、元素样式
需要同步读取布局后更新基于布局信息立即更新状态自适应组件、折叠面板
需要避免视觉闪烁DOM 更新后立即执行,用户看不到变化动画初始化、位置调整
第三方库需要同步初始化某些库必须在 DOM 准备好后立即初始化图表库、编辑器

性能对比示例

import { useState, useEffect, useLayoutEffect } from "react";

const PerformanceTest = () => {
  const [count, setCount] = useState(0);

  // useEffect:不阻塞渲染,用户体验更流畅
  useEffect(() => {
    // 模拟耗时操作
    const start = Date.now();
    while (Date.now() - start < 100) {} // 阻塞 100ms
    console.log("useEffect 执行完毕");
  }, [count]);

  // useLayoutEffect:阻塞渲染,用户会感觉卡顿
  useLayoutEffect(() => {
    // 模拟耗时操作
    const start = Date.now();
    while (Date.now() - start < 100) {} // 阻塞 100ms
    console.log("useLayoutEffect 执行完毕");
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

// 结果:
// - useEffect:点击按钮后,count 立即更新显示,100ms 后打印日志(流畅)
// - useLayoutEffect:点击按钮后,等待 100ms,然后 count 才显示(卡顿)

注意事项

  • ⚠️ 优先使用 useEffect

    // 默认情况下,使用 useEffect
    // 只有在确实需要同步执行时,才使用 useLayoutEffect
    
  • ⚠️ 服务端渲染 (SSR) 警告

    // useLayoutEffect 在服务端会产生警告,因为服务端没有 DOM
    // 如果需要兼容 SSR,可以这样处理:
    
    import { useEffect, useLayoutEffect } from "react";
    
    const useIsomorphicLayoutEffect =
      typeof window !== "undefined" ? useLayoutEffect : useEffect;
    
    // 使用
    useIsomorphicLayoutEffect(() => {
      // 代码
    }, []);
    
  • ⚠️ 避免耗时操作

    // ❌ 错误:useLayoutEffect 中执行耗时操作会阻塞渲染
    useLayoutEffect(() => {
      // 耗时计算
      for (let i = 0; i < 1000000; i++) {
        // ...
      }
    }, []);
    
    // ✅ 正确:耗时操作放到 useEffect
    useEffect(() => {
      // 耗时计算
      for (let i = 0; i < 1000000; i++) {
        // ...
      }
    }, []);
    
  • ⚠️ 不要过度优化

    // 大部分情况下,用户不会注意到 useEffect 的闪烁
    // 只有在确实出现视觉问题时,才切换到 useLayoutEffect
    
  • 测量 DOM 时使用

    // ✅ 适合使用 useLayoutEffect 的典型场景
    useLayoutEffect(() => {
      const rect = elementRef.current.getBoundingClientRect();
      setPosition(rect);
    }, []);
    
  • 调试技巧

    // 如果不确定是否需要 useLayoutEffect,可以先用 useEffect
    // 如果出现闪烁或布局跳动,再改为 useLayoutEffect
    
    // 调试时可以打印执行顺序
    useLayoutEffect(() => {
      console.log("useLayoutEffect - 在绘制前执行");
    });
    
    useEffect(() => {
      console.log("useEffect - 在绘制后执行");
    });
    

快速决策树

需要执行副作用?
  ↓
是否涉及 DOM 测量或修改?
  ↓ 否 → 使用 useEffect ✅
  ↓ 是
  ↓
用户是否会看到闪烁或布局跳动?
  ↓ 否 → 使用 useEffect ✅
  ↓ 是
  ↓
使用 useLayoutEffect ✅

总结

原则说明
默认使用 useEffect99% 的情况都应该用 useEffect
闪烁时用 useLayoutEffect只有在出现视觉问题时才切换
避免耗时操作useLayoutEffect 会阻塞渲染
测量布局用 useLayoutEffect获取尺寸、位置等布局信息时使用
SSR 需要特殊处理使用 useIsomorphicLayoutEffect

记住:useLayoutEffect 是 useEffect 的同步版本,用于需要在浏览器绘制前执行的场景。