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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 05:44:51