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

如何实现React组件点击外部按钮触发动画,中途改props重启动画

React组件:外部按钮触发动画+props变化时重置动画

我要实现一个React组件,满足两个核心要求:

  • 点击外部按钮触发组件动画
  • 动画执行中途修改组件props,动画必须从头重新执行
  • props的修改由另一个独立按钮触发

下面是我尝试的代码:

function Button() {
  const indexRef = useRef(0);
  const clickHandler = () => {
    indexRef.current++;
  }
  return (<button onClick={clickHandler}><SkillWindow data={somedata[indexRef]}/></button>)
}

interface SkillWindow extends DefaultProps {
  data: SkillData;
}
function SKillCard({ data, className }: SkillWindow) {
  const id: string = "skillcard--0";
  useEffect((): void => {
    const target: HTMLElement | null = document.getElementById(id);
    if (!target) return;
    target.addEventListener("click", (): void => {
      target.style.animationPlayState = "paused";
    });
    target.style.animation = "running";
  }, [data]);
  const { name, desc }: SkillData = data;
  const style: TailwindProperties = {
    xl: "xl:w-96 xl:ml-8  xl:pb-8  xl:flex xl:flex-col xl:items-center xl:gap-8",
    base: "bg-neutral-200 bg-opacity-75 _swash-in",
  };
  return (
    <section className={`${style.xl} ${style.base} ${className}`} id={id}>
      <WindowPanel />
      <div className="w-40 h-40 rounded-full bg-black" />
      <div className="flex flex-col gap-4 items-center">
        <h1 className="text-lg font-bold">{name}</h1>
        <h2 className="text-base font-normal">hello</h2>
        <p className="text-base font-normal">{desc}</p>
      </div>
    </section>
  );
}
export default SKillCard;

原代码存在的问题

  1. Button组件不触发重渲染:用indexRef存储索引不会触发组件更新,点击按钮后somedata[indexRef]无法把新数据传给SkillCard,props根本没变化。
  2. DOM操作不规范:用document.getElementById获取元素且未清理点击事件,易引发内存泄漏;另外target.style.animation = "running"是错误写法,应该控制动画播放状态或类名。
  3. 固定id冲突风险:多实例场景下,重复id会导致DOM选择错误。

解决方案

方案1:用key属性重置组件(最简洁)

React中组件key变化时,会卸载旧组件并重新挂载新组件,动画自动从头执行。同时Button组件改用state管理索引,确保点击触发重渲染。

import { useState, useRef, useEffect } from 'react';

// 修正后的Button组件
function Button({ somedata }: { somedata: SkillData[] }) {
  const [currentIndex, setCurrentIndex] = useState(0);
  
  const clickHandler = () => {
    // 循环切换数据,可根据需求修改逻辑
    setCurrentIndex(prev => (prev + 1) % somedata.length);
  };

  return (
    <button onClick={clickHandler}>
      <SkillCard 
        data={somedata[currentIndex]} 
        key={currentIndex} {/* 关键:index变化时触发组件重置 */}
      />
    </button>
  );
}

// 修正后的SkillCard组件
interface SkillCardProps extends DefaultProps {
  data: SkillData;
}

function SkillCard({ data, className }: SkillCardProps) {
  const { name, desc } = data;
  const cardRef = useRef<HTMLElement>(null);
  const style = {
    xl: "xl:w-96 xl:ml-8 xl:pb-8 xl:flex xl:flex-col xl:items-center xl:gap-8",
    base: "bg-neutral-200 bg-opacity-75 _swash-in",
  };

  // 点击暂停/恢复动画逻辑
  useEffect(() => {
    const handleClick = () => {
      if (cardRef.current) {
        const currentState = cardRef.current.style.animationPlayState;
        cardRef.current.style.animationPlayState = currentState === 'paused' ? 'running' : 'paused';
      }
    };
    const element = cardRef.current;
    if (element) {
      element.addEventListener('click', handleClick);
    }
    // 清理事件避免内存泄漏
    return () => {
      if (element) {
        element.removeEventListener('click', handleClick);
      }
    };
  }, []);

  return (
    <section 
      ref={cardRef}
      className={`${style.xl} ${style.base} ${className}`}
    >
      <WindowPanel />
      <div className="w-40 h-40 rounded-full bg-black" />
      <div className="flex flex-col gap-4 items-center">
        <h1 className="text-lg font-bold">{name}</h1>
        <h2 className="text-base font-normal">hello</h2>
        <p className="text-base font-normal">{desc}</p>
      </div>
    </section>
  );
}

export default SkillCard;

方案2:手动控制动画重置(不重新挂载组件)

如果不想重新挂载组件,可通过移除再添加动画类的方式重置动画,核心是强制浏览器重绘后再添加类:

import { useRef, useEffect } from 'react';

interface SkillCardProps extends DefaultProps {
  data: SkillData;
}

function SkillCard({ data, className }: SkillCardProps) {
  const cardRef = useRef<HTMLElement>(null);
  const { name, desc } = data;
  const style = {
    xl: "xl:w-96 xl:ml-8 xl:pb-8 xl:flex xl:flex-col xl:items-center xl:gap-8",
    base: "bg-neutral-200 bg-opacity-75",
    animation: "_swash-in",
  };

  useEffect(() => {
    const element = cardRef.current;
    if (!element) return;

    // 重置动画:先移除动画类
    element.classList.remove(style.animation);
    // 强制浏览器重绘,否则移除再添加可能不生效
    void element.offsetWidth;
    // 重新添加动画类,触发从头执行
    element.classList.add(style.animation);

    // 点击暂停/恢复逻辑
    const handleClick = () => {
      const currentState = element.style.animationPlayState;
      element.style.animationPlayState = currentState === 'paused' ? 'running' : 'paused';
    };
    element.addEventListener('click', handleClick);
    return () => element.removeEventListener('click', handleClick);
  }, [data]);

  return (
    <section 
      ref={cardRef}
      className={`${style.xl} ${style.base} ${className} ${style.animation}`}
    >
      <WindowPanel />
      <div className="w-40 h-40 rounded-full bg-black" />
      <div className="flex flex-col gap-4 items-center">
        <h1 className="text-lg font-bold">{name}</h1>
        <h2 className="text-base font-normal">hello</h2>
        <p className="text-base font-normal">{desc}</p>
      </div>
    </section>
  );
}

export default SkillCard;

内容的提问来源于stack exchange,提问作者レノキ乇1ᑌᑭ

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 14:35:00