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

React中使用useGSAP时,如何替代setTimeout确保DOM就绪?

React中useGSAP子组件无法获取父元素Ref的解决方案

在React项目中使用useGSAP实现动画时,子组件接收父组件传递的parentRef后,直接在useGSAP回调中调用动画初始化函数setupAnimation,会出现parentRef.current为null的情况,无法获取父元素宽度。目前通过setTimeout延迟执行可以临时解决,但需要更可靠的替代方案。

原代码如下:

parent.tsx

export default function Page() {
  const parentRef = useRef(null);

  return (
    <main className="flex min-h-screen flex-col p-6">
      <div
        className="flex h-20 shrink-0 items-end rounded-lg bg-dark-slate-gray p-4 md:h-52"
        ref={parentRef}
      >
        <Logo parentRef={parentRef} />
      </div>
    </main>
  );
}

child.tsx

export default function Logo({ parentRef }: LogoProps) {
  const container = useRef<HTMLDivElement>(null);

  const setupAnimation = () => {
    const parentWidth = parentRef.current?.getBoundingClientRect().width;
    gsap.to(container.current, { x: parentWidth, rotation: 10 });
  };

  useGSAP(
    () => {
      setupAnimation();
    },
    { scope: container }
  );

  return (
    <div
      className={`${lusitana.className} flex flex-row items-center leading-none text-wheat`}
      ref={container}
    >
      <GlobeAltIcon className="h-12 w-12 rotate-[15deg]" />
    </div>
  );
}

临时解决方案(使用setTimeout)

useGSAP(
  () => {
    setTimeout(() => {
      setupAnimation();
    }, 100);
  },
  { scope: container }
);

原因分析

出现parentRef.current为null的核心原因是:useGSAP的回调执行时机早于父组件Ref绑定到DOM元素的时机,或者父元素的DOM布局尚未完成(比如响应式样式还未生效),导致无法获取有效的DOM引用及尺寸。setTimeout本质是强制延迟到下一轮事件循环执行,避开了这个时机差,但属于不稳定的hack方案。


最优解决方案

方案1:利用useGSAP的依赖监听+Ref状态判断

通过在useGSAP的配置中添加依赖项,监听parentRef.current的变化,同时在回调中确保parentRef.current存在后再执行动画:

export default function Logo({ parentRef }: LogoProps) {
  const container = useRef<HTMLDivElement>(null);

  const setupAnimation = () => {
    if (!parentRef.current) return;
    const parentWidth = parentRef.current.getBoundingClientRect().width;
    gsap.to(container.current, { x: parentWidth, rotation: 10 });
  };

  useGSAP(
    () => {
      setupAnimation();
    },
    { 
      scope: container,
      deps: [parentRef.current] // 监听parentRef.current的变化
    }
  );

  return (
    // ... 原有JSX结构
  );
}

方案2:通过父组件传递父元素宽度(更直接)

避免子组件依赖父元素的Ref,改为由父组件计算好宽度后通过props传递给子组件,这样子组件可以直接使用props中的值,无需处理Ref的时机问题:

parent.tsx 改造

export default function Page() {
  const parentRef = useRef(null);
  const [parentWidth, setParentWidth] = useState(0);

  useEffect(() => {
    if (parentRef.current) {
      setParentWidth(parentRef.current.getBoundingClientRect().width);
      // 可选:监听窗口 resize 更新宽度
      const handleResize = () => {
        setParentWidth(parentRef.current!.getBoundingClientRect().width);
      };
      window.addEventListener('resize', handleResize);
      return () => window.removeEventListener('resize', handleResize);
    }
  }, []);

  return (
    <main className="flex min-h-screen flex-col p-6">
      <div
        className="flex h-20 shrink-0 items-end rounded-lg bg-dark-slate-gray p-4 md:h-52"
        ref={parentRef}
      >
        <Logo parentWidth={parentWidth} />
      </div>
    </main>
  );
}

child.tsx 改造

export default function Logo({ parentWidth }: LogoProps) {
  const container = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      if (parentWidth === 0) return;
      gsap.to(container.current, { x: parentWidth, rotation: 10 });
    },
    { 
      scope: container,
      deps: [parentWidth] // 监听宽度变化
    }
  );

  return (
    // ... 原有JSX结构
  );
}

方案3:使用useLayoutEffect确保DOM布局完成

如果是因为DOM布局未完成导致无法获取正确宽度,可以结合useLayoutEffect(在DOM布局完成后立即执行)来触发动画:

export default function Logo({ parentRef }: LogoProps) {
  const container = useRef<HTMLDivElement>(null);
  const animationTriggered = useRef(false);

  useLayoutEffect(() => {
    if (!parentRef.current || animationTriggered.current) return;
    const parentWidth = parentRef.current.getBoundingClientRect().width;
    gsap.to(container.current, { x: parentWidth, rotation: 10 });
    animationTriggered.current = true;
  }, [parentRef]);

  return (
    // ... 原有JSX结构
  );
}

方案选择建议

  • 如果仅需要初始化一次动画,方案1足够简洁,利用useGSAP的依赖监听自动处理时机。
  • 如果动画需要响应父元素尺寸变化(比如窗口resize),方案2更合适,父组件统一管理尺寸状态,子组件只需响应props变化。
  • 如果是DOM布局时机问题,方案3的useLayoutEffect能确保在DOM布局完成后立即执行动画,避免尺寸计算偏差。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 04:34:54