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

React.js使用CountUp库出现'target is null or undefined'错误求助

问题:react-countup库出现'target is null or undefined'错误

在Next.js(React)项目中使用react-countup库时,持续遇到target is null or undefined错误。已尝试通过isReady状态判断组件渲染时机,但问题仍未解决,无法确定错误来自自身代码还是库本身,需要排查修复。

现有代码

"use client";
import React, { useEffect, useRef, useState } from "react";
import CountUp from "react-countup";
import Image from "next/image";
import style from "../styles/about.module.scss";
import me from "../../public/me.png";
import AnimeButton from "./AnimeButton";
import Link from "next/link";

const About = ({ setReferences }) => {
  const boxRef = useRef();

  // 标记组件是否准备好渲染CountUp
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    setReferences((prev) => {
      prev[1] = boxRef.current;
      return prev;
    });

    setIsReady(true);
  }, []);
  return (
    <section id="about" className={style.about_section} ref={boxRef}>
      <div className={style.particle_animation}></div>
      <h1 className={style.about_heading}> more about me</h1>

      <div className={style.about_container}>
        <div className={style.about_details}>
          <p>
            <span>
              Hey, I'm <span>Donkor Pius</span> , a front-end developer
              passionate about crafting captivating online experiences. With a
              creative mind and coding skills, I turn designs into functional
              and visually appealing websites. My focus on responsive design
              ensures seamless browsing across devices. Let's collaborate and
              bring your digital ideas to life.
            </span>
            <Link style={{ textDecoration: "none" }} href="#">
              <AnimeButton text="download my resume here" animeColor={1} />
            </Link>
          </p>
        </div>

        <div className={style.about_image_div}>
          <div className={style.ImageDiv_styler}></div>
          <Image className={style.about_image} src={me} alt="my image" />
        </div>

        <div className={style.about_other_details}>
          <div className={style.number_count_div}>
            {isReady && (
              <>
                <p>PROJECTS DONE</p>
                <span>
                  <CountUp
                    start={0}
                    end={30}
                    duration={2}
                    enableScrollSpy={true}
                    scrollSpyDelay={100}
                    separator=","
                  />
                  {" +"}
                </span>
              </>
            )}
          </div>
          <div className={style.number_count_div}>
            {isReady && (
              <>
                <p>SATISFIED CLIENTS</p>
                <span>
                  <CountUp
                    start={0}
                    end={10}
                    duration={2}
                    enableScrollSpy={true}
                    scrollSpyDelay={100}
                    separator=","
                  />
                  {" +"}
                </span>
              </>
            )}
          </div>
          <div className={style.number_count_div}>
            {isReady && (
              <>
                <p>YEARS OF EXPERIENCE</p>
                <span>
                  <CountUp
                    start={0}
                    end={3}
                    duration={2}
                    enableScrollSpy={true}
                    scrollSpyDelay={100}
                    separator=","
                  />
                  {" +"}
                </span>
              </>
            )}
          </div>
        </div>
      </div>
    </section>
  );
};

export default About;

问题根源与修复方案

问题原因

enableScrollSpy功能依赖CountUp组件的DOM节点完成初始化,但Next.js客户端组件的渲染流程中,isReady的触发时机(空依赖useEffect)早于CountUp的DOM节点挂载,导致scrollSpy初始化时找不到目标节点,抛出target is null or undefined错误。

修复方案1:基于CountUp的DOM节点挂载状态控制渲染

给每个CountUp组件添加ref,监听DOM节点是否存在,确保节点挂载后再启用scrollSpy:

"use client";
import React, { useEffect, useRef, useState } from "react";
import CountUp from "react-countup";
import Image from "next/image";
import style from "../styles/about.module.scss";
import me from "../../public/me.png";
import AnimeButton from "./AnimeButton";
import Link from "next/link";

const About = ({ setReferences }) => {
  const boxRef = useRef();
  const countUpRefs = [useRef(null), useRef(null), useRef(null)];
  const [countUpsReady, setCountUpsReady] = useState([false, false, false]);

  useEffect(() => {
    setReferences((prev) => {
      prev[1] = boxRef.current;
      return prev;
    });

    // 监听每个CountUp的DOM节点是否挂载完成
    countUpRefs.forEach((ref, index) => {
      const observer = new MutationObserver(() => {
        if (ref.current?.container) {
          setCountUpsReady(prev => {
            const newState = [...prev];
            newState[index] = true;
            return newState;
          });
          observer.disconnect();
        }
      });
      if (ref.current) {
        observer.observe(ref.current, { childList: true, subtree: true });
      }
    });
  }, []);

  return (
    <section id="about" className={style.about_section} ref={boxRef}>
      <div className={style.particle_animation}></div>
      <h1 className={style.about_heading}> more about me</h1>

      <div className={style.about_container}>
        <div className={style.about_details}>
          <p>
            <span>
              Hey, I'm <span>Donkor Pius</span> , a front-end developer
              passionate about crafting captivating online experiences. With a
              creative mind and coding skills, I turn designs into functional
              and visually appealing websites. My focus on responsive design
              ensures seamless browsing across devices. Let's collaborate and
              bring your digital ideas to life.
            </span>
            <Link style={{ textDecoration: "none" }} href="#">
              <AnimeButton text="download my resume here" animeColor={1} />
            </Link>
          </p>
        </div>

        <div className={style.about_image_div}>
          <div className={style.ImageDiv_styler}></div>
          <Image className={style.about_image} src={me} alt="my image" />
        </div>

        <div className={style.about_other_details}>
          <div className={style.number_count_div}>
            <p>PROJECTS DONE</p>
            <span>
              <CountUp
                ref={countUpRefs[0]}
                start={0}
                end={30}
                duration={2}
                enableScrollSpy={countUpsReady[0]}
                scrollSpyDelay={100}
                separator=","
              />
              {" +"}
            </span>
          </div>
          <div className={style.number_count_div}>
            <p>SATISFIED CLIENTS</p>
            <span>
              <CountUp
                ref={countUpRefs[1]}
                start={0}
                end={10}
                duration={2}
                enableScrollSpy={countUpsReady[1]}
                scrollSpyDelay={100}
                separator=","
              />
              {" +"}
            </span>
          </div>
          <div className={style.number_count_div}>
            <p>YEARS OF EXPERIENCE</p>
            <span>
              <CountUp
                ref={countUpRefs[2]}
                start={0}
                end={3}
                duration={2}
                enableScrollSpy={countUpsReady[2]}
                scrollSpyDelay={100}
                separator=","
              />
              {" +"}
            </span>
          </div>
        </div>
      </div>
    </section>
  );
};

export default About;

修复方案2:手动实现滚动监听,触发CountUp

放弃依赖库的scrollSpy功能,自己通过IntersectionObserver监听元素是否进入视口,再启动CountUp:

"use client";
import React, { useEffect, useRef, useState } from "react";
import CountUp from "react-countup";
import Image from "next/image";
import style from "../styles/about.module.scss";
import me from "../../public/me.png";
import AnimeButton from "./AnimeButton";
import Link from "next/link";

const About = ({ setReferences }) => {
  const boxRef = useRef();
  const countersRef = useRef(null);
  const [startCount, setStartCount] = useState(false);

  useEffect(() => {
    setReferences((prev) => {
      prev[1] = boxRef.current;
      return prev;
    });

    // 监听计数器区域是否进入视口
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setStartCount(true);
          observer.disconnect();
        }
      },
      { threshold: 0.5 }
    );

    if (countersRef.current) {
      observer.observe(countersRef.current);
    }

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

  return (
    <section id="about" className={style.about_section} ref={boxRef}>
      <div className={style.particle_animation}></div>
      <h1 className={style.about_heading}> more about me</h1>

      <div className={style.about_container}>
        <div className={style.about_details}>
          <p>
            <span>
              Hey, I'm <span>Donkor Pius</span> , a front-end developer
              passionate about crafting captivating online experiences. With a
              creative mind and coding skills, I turn designs into functional
              and visually appealing websites. My focus on responsive design
              ensures seamless browsing across devices. Let's collaborate and
              bring your digital ideas to life.
            </span>
            <Link style={{ textDecoration: "none" }} href="#">
              <AnimeButton text="download my resume here" animeColor={1} />
            </Link>
          </p>
        </div>

        <div className={style.about_image_div}>
          <div className={style.ImageDiv_styler}></div>
          <Image className={style.about_image} src={me} alt="my image" />
        </div>

        <div className={style.about_other_details} ref={countersRef}>
          <div className={style.number_count_div}>
            <p>PROJECTS DONE</p>
            <span>
              {startCount && (
                <CountUp
                  start={0}
                  end={30}
                  duration={2}
                  separator=","
                />
              )}
              {" +"}
            </span>
          </div>
          <div className={style.number_count_div}>
            <p>SATISFIED CLIENTS</p>
            <span>
              {startCount && (
                <CountUp
                  start={0}
                  end={10}
                  duration={2}
                  separator=","
                />
              )}
              {" +"}
            </span>
          </div>
          <div className={style.number_count_div}>
            <p>YEARS OF EXPERIENCE</p>
            <span>
              {startCount && (
                <CountUp
                  start={0}
                  end={3}
                  duration={2}
                  separator=","
                />
              )}
              {" +"}
            </span>
          </div>
        </div>
      </div>
    </section>
  );
};

export default About;

方案选择

  • 方案1保留了react-countup的原生scrollSpy功能,适合需要精确控制滚动触发时机的场景;
  • 方案2实现更简洁,不依赖库的内部逻辑,稳定性更高,推荐优先尝试。

内容的提问来源于stack exchange,提问作者Pius Donkor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 13:12:05