移除setTimeout:修复React useFadeElements Hook DOM就绪前失效问题
问题修复:useFadeElements Hook 在添加 OverviewBox 后失效的解决方案
问题背景
我写了一个useFadeElements Hook,用来计算指定父元素下所有元素到页面顶部的距离,当元素距离视口底部不足100px时,给元素添加hidden类实现淡出效果。原本运行正常,但添加OverviewBox组件后Hook失效,目前只能通过嵌套两层setTimeout临时恢复功能,需要找到不用setTimeout的合理修复方案。
问题根源
- OverviewBox的异步渲染:原
OverviewBox组件使用useState+useEffect异步设置标题标签,导致组件挂载后DOM不会立即生成,此时useFadeElementsHook去获取父元素内的DOM时,无法拿到OverviewBox内部的元素。 - 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
相关产品推荐
相关产品推荐

