You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

移除setTimeout:修复React useFadeElements Hook DOM就绪前失效问题

问题修复:useFadeElements Hook 在添加 OverviewBox 后失效的解决方案

问题背景

我写了一个useFadeElements Hook,用来计算指定父元素下所有元素到页面顶部的距离,当元素距离视口底部不足100px时,给元素添加hidden类实现淡出效果。原本运行正常,但添加OverviewBox组件后Hook失效,目前只能通过嵌套两层setTimeout临时恢复功能,需要找到不用setTimeout的合理修复方案。

问题根源

  1. OverviewBox的异步渲染:原OverviewBox组件使用useState+useEffect异步设置标题标签,导致组件挂载后DOM不会立即生成,此时useFadeElements Hook去获取父元素内的DOM时,无法拿到OverviewBox内部的元素。
  2. useFadeElements的DOM时机问题:原Hook仅在组件挂载时(空依赖useEffect)执行一次DOM查询,无法感知后续OverviewBox异步渲染带来的DOM变化。

解决方案

1. 修复OverviewBox组件(去掉异步渲染逻辑)

titleSize是传入的props,完全可以同步使用,不需要用useState和useEffect异步处理,直接在渲染时动态生成标题标签即可,确保组件挂载后DOM立即生成。

2. 优化useFadeElements Hook

  • 移除嵌套setTimeout,改用MutationObserver监听父元素的DOM变化,当子节点新增/移除时重新计算元素距离;
  • 优化滚动监听的添加与移除逻辑,确保移除的是同一个处理函数,避免内存泄漏。

修改后的代码

修复后的OverviewBox组件

import "./OverviewBox.css"

const OverviewBox = ({title, info, date, imgSrc, titleSize="h2"}) => {
  // 直接使用props传入的titleSize,无需异步处理
  const TitleTag = titleSize;

  return (
    <div className="overview-box">
      <TitleTag className="overview-box__title">{title}</TitleTag>
      <p className="overview-box__info">{info}</p>
      <img className="overview-box__img" src={imgSrc} alt="img"/>
      <span className="overview-box__date">{date}</span>
    </div>
  );
};

export default OverviewBox;

优化后的useFadeElements Hook

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

export const useFadeElements = (parentElementId, excludeTags) => {
  const [elementsAndDistances, setElementsAndDistances] = useState(null);

  // 提取计算元素距离的逻辑为单独函数
  const calculateElementsAndDistances = useCallback(() => {
    const targetElement = document.getElementById(parentElementId);
    if (!targetElement) return;

    let elementsInside = [...targetElement.getElementsByTagName("*")];

    // 过滤需要排除的标签(统一转为大写避免大小写问题)
    if (excludeTags.length > 0) {
      elementsInside = elementsInside.filter(el => 
        !excludeTags.includes(el.tagName.toUpperCase())
      );
    }

    const elementsData = elementsInside.map(el => {
      el.classList.add("unhidden");
      
      let distance = 0;
      let temporaryEl = el;
      do {
        distance += temporaryEl.offsetTop;
        temporaryEl = temporaryEl.offsetParent;
      } while (temporaryEl);
      
      distance = Math.max(0, distance);
      return { el, distance };
    });

    setElementsAndDistances(elementsData);
  }, [parentElementId, excludeTags]);

  useEffect(() => {
    // 初始计算一次
    calculateElementsAndDistances();

    // 监听父元素的DOM变化,重新计算元素数据
    const targetElement = document.getElementById(parentElementId);
    if (!targetElement) return;

    const observer = new MutationObserver(calculateElementsAndDistances);
    observer.observe(targetElement, {
      childList: true,
      subtree: true,
    });

    return () => observer.disconnect();
  }, [calculateElementsAndDistances, parentElementId]);

  // 滚动监听逻辑
  useEffect(() => {
    if (!elementsAndDistances) return;

    const checkElementDistance = (elementAndDistance) => {
      const viewportBottom = window.scrollY + window.innerHeight;
      if (viewportBottom - 100 < elementAndDistance.distance) {
        elementAndDistance.el.classList.add("hidden");
      } else {
        elementAndDistance.el.classList.remove("hidden");
      }
    };

    const handleScroll = () => {
      elementsAndDistances.forEach(checkElementDistance);
    };

    window.addEventListener("scroll", handleScroll);
    // 初始执行一次,确保页面加载时元素状态正确
    handleScroll();

    return () => window.removeEventListener("scroll", handleScroll);
  }, [elementsAndDistances]);

  return { elementsAndDistances };
};

关键改进点

  • OverviewBox同步渲染:移除不必要的异步状态逻辑,确保DOM在组件挂载时立即生成;
  • MutationObserver监听DOM变化:让Hook能感知父元素内的DOM更新,自动重新计算元素距离;
  • 滚动监听优化:提取独立的处理函数,确保添加和移除的是同一个函数,避免内存泄漏;
  • 初始状态触发:滚动监听添加后立即执行一次,确保页面加载时元素状态正确。

内容的提问来源于stack exchange,提问作者Jakub Godlewski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 05:25:38