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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 02:58:11