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
相关产品推荐
相关产品推荐

