Bootstrap

React 中hooks之useLayoutEffect 用法总结以及与useEffect的区别

React useLayoutEffect

1. useLayoutEffect 基本概念

useLayoutEffect 是 React 的一个 Hook,它的函数签名与 useEffect 完全相同,但它会在所有的 DOM 变更之后同步调用 effect。它可以用来读取 DOM 布局并同步触发重渲染。

2. useLayoutEffect vs useEffect

2.1 执行时机对比

Hook 名称执行时机执行方式使用场景
useEffectDOM 更新后且浏览器重新绘制屏幕之后异步执行 (组件渲染完成后)异步执行,不阻塞浏览器渲染大多数副作用,如数据获取、订阅
useLayoutEffectDOM 更新后且浏览器重新绘制屏幕之前同步执行(组件将要渲染时)同步执行,会阻塞浏览器渲染需要同步测量 DOM 或更新布局

2.2 执行顺序示例

function ExampleComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('useEffect 执行'); // 后执行
  });

  useLayoutEffect(() => {
    console.log('useLayoutEffect 执行'); // 先执行
  });

  return (
    <div onClick={() => setCount(c => c + 1)}>
      点击次数:{count}
    </div>
  );
}

3. useLayoutEffect 使用场景

3.1 DOM 测量和更新

function AutoHeight() {
  const [height, setHeight] = useState(0);
  const elementRef = useRef();

  useLayoutEffect(() => {
    // 在 DOM 更新后立即测量高度
    const element = elementRef.current;
    const elementHeight = element.getBoundingClientRect().height;
    
    if (elementHeight !== height) {
      // 立即更新高度,避免闪烁
      setHeight(elementHeight);
    }
  }, [height]);

  return (
    <div>
      <div ref={elementRef} style={{ height: height || 'auto' }}>
        内容
      </div>
      <div>当前高度: {height}px</div>
    </div>
  );
}

3.2 防止闪烁的工具提示

function Tooltip({ text, position }) {
  const tooltipRef = useRef();
  const [tooltipPosition, setTooltipPosition] = useState(position);

  useLayoutEffect(() => {
    const tooltip = tooltipRef.current;
    const rect = tooltip.getBoundingClientRect();
    
    // 检查是否超出视口
    if (rect.right > window.innerWidth) {
      // 立即调整位置,避免闪烁
      setTooltipPosition({
        ...position,
        left: position.left - (rect.right - window.innerWidth)
      });
    }
  }, [position]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'absolute',
        ...tooltipPosition
      }}
    >
      {text}
    </div>
  );
}

3.3 动画处理

function AnimatedComponent() {
  const elementRef = useRef();
  const [isVisible, setIsVisible] = useState(false);

  useLayoutEffect(() => {
    if (isVisible) {
      const element = elementRef.current;
      // 立即设置初始状态
      element.style.opacity = '0';
      element.style.transform = 'translateY(20px)';
      
      // 强制重排
      element.getBoundingClientRect();
      
      // 应用动画
      element.style.transition = 'opacity 0.5s, transform 0.5s';
      element.style.opacity = '1';
      element.style.transform = 'translateY(0)';
    }
  }, [isVisible]);

  return (
    <div>
      <button onClick={() => setIsVisible(true)}>显示</button>
      <div ref={elementRef}>
        动画内容
      </div>
    </div>
  );
}

3.4 滚动位置同步

function ScrollSync({ content }) {
  const scrollContainerRef = useRef();

  useLayoutEffect(() => {
    const element = scrollContainerRef.current;
    // 内容更新后立即滚动到底部
    element.scrollTop = element.scrollHeight;
  }, [content]); // 当内容更新时执行

  return (
    <div 
      ref={scrollContainerRef}
      style={{ 
        height: '200px', 
        overflow: 'auto' 
      }}
    >
      {content.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
}

3.5 Modal 定位

function Modal({ isOpen, children }) {
  const modalRef = useRef();

  useLayoutEffect(() => {
    if (isOpen) {
      const modalElement = modalRef.current;
      const viewportHeight = window.innerHeight;
      const modalHeight = modalElement.getBoundingClientRect().height;
      
      // 立即计算并设置最佳位置
      modalElement.style.top = \`\${Math.max(
        0,
        (viewportHeight - modalHeight) / 2
      )}px\`;
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div ref={modalRef} className="modal">
        {children}
      </div>
    </div>
  );
}

4. 性能考虑

4.1 何时使用 useLayoutEffect

  • 需要同步测量 DOM 元素
  • 需要在视觉更新前进行 DOM 修改
  • 需要避免闪烁或布局抖动
  • 处理依赖于 DOM 布局的动画

4.2 何时使用 useEffect

  • 数据获取
  • 订阅事件
  • 日志记录
  • 其他不需要同步 DOM 测量或修改的副作用

5. 最佳实践

  1. 优先使用 useEffect
// ✅ 大多数情况下使用 useEffect 即可
useEffect(() => {
  // 异步操作,不影响渲染
  fetchData();
}, []);
  1. 仅在必要时使用 useLayoutEffect
// ✅ 需要同步 DOM 测量和更新时使用 useLayoutEffect
useLayoutEffect(() => {
  // 同步操作,立即更新 DOM
  updateDOMPosition();
}, []);
  1. 注意性能影响
// ❌ 避免在 useLayoutEffect 中进行耗时操作
useLayoutEffect(() => {
  // 不要在这里进行大量计算或 API 调用
  heavyComputation();
}, []);

// ✅ 耗时操作应该放在 useEffect 中
useEffect(() => {
  heavyComputation();
}, []);
  1. 合理使用依赖项
function OptimizedComponent({ data }) {
  useLayoutEffect(() => {
    // 只在真正需要同步更新的依赖项发生变化时执行
  }, [data.layout]); // 只依赖布局相关的属性
}

6. 注意事项

  1. useLayoutEffect 在服务器端渲染(SSR)中会收到警告,因为它只能在客户端执行
  2. 过度使用 useLayoutEffect 可能会导致性能问题
  3. 应该将耗时的操作放在 useEffect 中,只在 useLayoutEffect 中处理视觉相关的同步更新
  4. 在条件语句中使用时需要注意 Hook 规则

通过合理使用 useLayoutEffect 和 useEffect,我们可以更好地控制副作用的执行时机,优化用户体验,同时保持应用的性能。在实际开发中,应该根据具体场景选择合适的 Hook。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;