NextJS(TS)页面加载组件问题:load事件未触发无法切换状态
问题分析与解决方案
你的代码存在几个核心问题,导致load事件未触发或状态判断失效:
1. 闭包与状态更新时机问题
在useEffect中调用setResourcesNum后,当前闭包内的resoucesNum仍为初始值0,后续resourceLoaded函数里的loadedResources === resoucesNum判断永远无法成立,因为此时resoucesNum并未更新为实际资源总数。
2. 缓存资源的load事件不触发
若图片/视频已被浏览器缓存,它们的load/loadeddata事件不会再次触发,导致计数永远缺项,加载状态无法切换。
3. Next.js Image组件的特殊性
如果使用Next.js官方的<Image>组件而非原生<img>,直接通过document.querySelectorAll("img")无法正确捕获实际的img元素(Next.js Image会做封装和懒加载处理)。
修正后的实现方案
方案1:针对原生<img>和<video>场景
import { useState, useEffect, useRef } from 'react'; const Loading: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [loaded, setLoaded] = useState(false); // 用ref存储计数,避免闭包导致的状态值过期问题 const loadedResourcesRef = useRef(0); const totalResourcesRef = useRef(0); useEffect(() => { const images = Array.from(document.querySelectorAll("img")); const videos = Array.from(document.querySelectorAll("video")); totalResourcesRef.current = images.length + videos.length; const handleResourceLoaded = () => { loadedResourcesRef.current++; if (loadedResourcesRef.current === totalResourcesRef.current) { setLoaded(true); } }; // 处理图片:包含缓存已完成的情况 images.forEach(image => { if (image.complete) { handleResourceLoaded(); } else { image.addEventListener("load", handleResourceLoaded); // 监听加载失败,避免单个资源卡住加载状态 image.addEventListener("error", handleResourceLoaded); } }); // 处理视频:包含缓存已完成的情况 videos.forEach(video => { // readyState >=2 对应loadeddata状态 if (video.readyState >= 2) { handleResourceLoaded(); } else { video.addEventListener("loadeddata", handleResourceLoaded); video.addEventListener("error", handleResourceLoaded); } }); // 清理事件监听 return () => { images.forEach(image => { image.removeEventListener("load", handleResourceLoaded); image.removeEventListener("error", handleResourceLoaded); }); videos.forEach(video => { video.removeEventListener("loadeddata", handleResourceLoaded); video.removeEventListener("error", handleResourceLoaded); }); }; }, []); // 无资源时直接标记加载完成 useEffect(() => { if (totalResourcesRef.current === 0) { setLoaded(true); } }, []); return ( <> {!loaded && ( <div className="w-full h-screen fixed bg-white flex justify-center items-center z-[9999]"> <h1>Loading</h1> </div> )} {children} </> ); }; export default Loading;
方案2:针对Next.js <Image>组件场景
如果项目使用Next.js的<Image>,建议直接通过组件的onLoad属性传递回调,更可靠:
import { useState, useRef } from 'react'; import Image from 'next/image'; // 封装带加载回调的Image组件 const TrackedImage = ({ onLoad, register, ...props }) => { register(); return <Image {...props} onLoad={onLoad} />; }; const Loading: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [loaded, setLoaded] = useState(false); const loadedResourcesRef = useRef(0); const totalResourcesRef = useRef(0); // 注册资源总数 const registerResource = () => { totalResourcesRef.current++; }; // 标记资源加载完成 const markResourceLoaded = () => { loadedResourcesRef.current++; if (loadedResourcesRef.current === totalResourcesRef.current) { setLoaded(true); } }; // 处理无资源的情况 setTimeout(() => { if (totalResourcesRef.current === 0) { setLoaded(true); } }, 100); return ( <> {!loaded && ( <div className="w-full h-screen fixed bg-white flex justify-center items-center z-[9999]"> <h1>Loading</h1> </div> )} {/* 遍历子组件,为资源组件添加追踪逻辑 */} {React.Children.map(children, child => { if (React.isValidElement(child)) { if (child.type === TrackedImage) { return React.cloneElement(child, { onLoad: markResourceLoaded, register: registerResource, }); } // 同理处理原生video组件 if (child.type === 'video') { registerResource(); return React.cloneElement(child, { onLoadedData: markResourceLoaded, onError: markResourceLoaded, }); } } return child; })} </> ); }; export default Loading;
关键优化点
- 使用
useRef存储计数,避免闭包导致的状态值过期 - 处理缓存资源的加载完成状态,避免遗漏计数
- 添加
error事件监听,防止单个资源加载失败导致加载状态卡住 - 针对Next.js特殊组件做适配,避免DOM查询的局限性
内容的提问来源于stack exchange,提问作者Codex
相关产品推荐
相关产品推荐

