如何实现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;
原代码存在的问题
- Button组件不触发重渲染:用
indexRef存储索引不会触发组件更新,点击按钮后somedata[indexRef]无法把新数据传给SkillCard,props根本没变化。 - DOM操作不规范:用
document.getElementById获取元素且未清理点击事件,易引发内存泄漏;另外target.style.animation = "running"是错误写法,应该控制动画播放状态或类名。 - 固定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ᑌᑭ
相关产品推荐
相关产品推荐

