React中如何在元素渲染完成后执行useGSAP Hook?
解决GSAP动画因DOM未更新导致宽度获取失败的问题
问题根源
useGSAP钩子默认在组件挂载时执行,但你是通过innerHTML动态往#membersBox里添加内容,此时DOM还没完成重绘计算,所以获取到的clientWidth为0,直接导致动画参数错误、无法正常运行。
解决方案一:用React状态管理渲染动态内容(推荐)
放弃手动操作innerHTML,改用React状态追踪动态元素,让框架自动处理DOM更新时机,再让useGSAP依赖状态确保在内容渲染完成后执行。
1. 重构状态与数据处理
import { useEffect, useState, useRef } from 'react' import gsap from "gsap"; import { useGSAP } from "@gsap/react"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import './membersPage.css' import data from './members.json' const MembersPage = () => { gsap.registerPlugin(ScrollTrigger); const [team, setTeam] = useState(''); const [members, setMembers] = useState([]); const [waves, setWaves] = useState([]); const membersBoxRef = useRef(null); // 用ref替代document.querySelector // 处理成员和波浪数据 useEffect(() => { const webData = data['web']; // 生成波浪数据 let waveCount = webData.length; if (waveCount % 2 !== 0) waveCount += 1; const newWaves = Array.from({length: waveCount}, (_, i) => ({ id: `wave${i}`, left: i === 0 ? 0 : -i * 28 })); setWaves(newWaves); // 生成成员数据 const newMembers = webData.map((e, i) => { if (i === 0) { return { ...e, left: 70, flexDirection: 'column-reverse', bottom: 0 }; } const prevLeft = newMembers[i-1].left; return { ...e, left: prevLeft + 135, flexDirection: i % 2 !== 0 ? 'column' : 'column-reverse', bottom: i % 2 === 0 ? 0 : -250 }; }); setMembers(newMembers); }, []); // 依赖members状态,确保DOM更新后执行GSAP useGSAP(() => { if (!membersBoxRef.current) return; const H = membersBoxRef.current.clientWidth; if (H === 0) return; // 宽度为0时跳过 let mm = gsap.matchMedia(); mm.add("(max-width: 767px)", () => { gsap.to(".teamBox", { x: -H - 1300, scrollTrigger: { trigger: ".teamWrap", scroller: "body", start: "top 0%", end: `top -${H / 5}%`, scrub: 2, pin: true, }, }); }); mm.add("(min-width: 768px)", () => { gsap.to(".teamBox", { x: -H - 300, scrollTrigger: { trigger: ".teamWrap", scroller: "body", start: "top 0%", end: `top -${H / 2}%`, scrub: 2, pin: true, }, }); }); // 清理动画实例,避免内存泄漏 return () => mm.revert(); }, [members]); // 渲染部分 return ( <> <div className='teamWrap'> <div className="teamCont"> <div className="teamHead"> <h1>Meet the minds behind the <span style={{ position: 'absolute', paddingLeft: '17px' }} >magic</span></h1> <h1 className='teamNameHead' >The <span className='headHighlight'>{team}</span>{team.toLowerCase() === "board" ? " Members" : " Team"}</h1> </div> <div className="teamBox"> <div id='wave' className="wave"> {waves.map(wave => ( <div key={wave.id} className="waveBox"> <img id={wave.id} style={{position: 'relative', left: `${wave.left}px`}} src="/PublicAssets/teamWave.svg" alt="" /> </div> ))} </div> <div id="membersBox" ref={membersBoxRef}> {members.map((member, i) => ( <div key={`member${i}`} className="member" id={`member${i}`} style={{ left: `${member.left}px`, flexDirection: member.flexDirection, bottom: `${member.bottom}px` }} > <div className="teamDetailCont"> <div className="memName">{member.name}</div> <div className="msg">"${member.msg}"</div> </div> <div className="mask"> <img className="teamImg" src={member.img} alt="" /> </div> </div> ))} </div> </div> </div> </div> </> ) } export default MembersPage
关键优化点:
- 用React状态
members、waves管理动态内容,让框架自动处理DOM更新 - 用
useRef获取#membersBox,替代document.querySelector更可靠 useGSAP依赖members状态,确保在成员渲染完成后执行- 替换重复的
id为class(避免DOM选择冲突) - 添加清理函数
mm.revert(),防止重复创建动画实例
解决方案二:保留innerHTML,等待DOM重绘
如果不想重构渲染逻辑,可以在动态添加内容后,等待DOM完成重绘再执行GSAP:
const [contentLoaded, setContentLoaded] = useState(false); useEffect(() => { // 原有的动态添加wave和member的代码... // 等待DOM重绘完成后标记状态 requestAnimationFrame(() => { setContentLoaded(true); }); }, []); useGSAP(() => { if (!contentLoaded) return; const H = document.querySelector("#membersBox").clientWidth; if (H === 0) return; // 原有的GSAP动画代码... }, [contentLoaded]);
内容的提问来源于stack exchange,提问作者Sanidhya Sahu
相关产品推荐
相关产品推荐

