GSAP 与 React 集成
如何在 React 项目中使用 GSAP 创建动画效果。
📚 React 集成概述
为什么需要特殊处理?
React 的虚拟 DOM 和组件生命周期需要特殊的集成方式:
- useRef:获取 DOM 元素引用
- useEffect:在组件挂载后初始化动画
- 清理函数:组件卸载时清理动画
🚀 基础集成
安装
npm install gsap基础示例
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
function AnimatedBox() {
const boxRef = useRef(null);
useEffect(() => {
gsap.to(boxRef.current, {
x: 100,
duration: 1,
ease: "power2.out"
});
}, []);
return <div ref={boxRef} className="box">动画盒子</div>;
}🎯 使用 useRef 获取元素
单个元素
function FadeInBox() {
const boxRef = useRef(null);
useEffect(() => {
gsap.from(boxRef.current, {
opacity: 0,
y: 50,
duration: 1
});
}, []);
return <div ref={boxRef}>淡入盒子</div>;
}多个元素
function MultipleBoxes() {
const box1Ref = useRef(null);
const box2Ref = useRef(null);
const box3Ref = useRef(null);
useEffect(() => {
const tl = gsap.timeline();
tl.to(box1Ref.current, { x: 100, duration: 1 })
.to(box2Ref.current, { y: 100, duration: 1 }, "-=0.5")
.to(box3Ref.current, { rotation: 360, duration: 1 });
}, []);
return (
<>
<div ref={box1Ref}>盒子 1</div>
<div ref={box2Ref}>盒子 2</div>
<div ref={box3Ref}>盒子 3</div>
</>
);
}使用 querySelector
function QuerySelectorExample() {
useEffect(() => {
const boxes = document.querySelectorAll(".box");
gsap.from(boxes, {
opacity: 0,
y: 50,
duration: 1,
stagger: 0.1
});
}, []);
return (
<>
<div className="box">盒子 1</div>
<div className="box">盒子 2</div>
<div className="box">盒子 3</div>
</>
);
}🎨 动画清理
基础清理
function AnimatedComponent() {
const boxRef = useRef(null);
const animationRef = useRef(null);
useEffect(() => {
// 创建动画
animationRef.current = gsap.to(boxRef.current, {
x: 100,
duration: 1
});
// 清理函数
return () => {
if (animationRef.current) {
animationRef.current.kill();
}
};
}, []);
return <div ref={boxRef}>动画元素</div>;
}Timeline 清理
function TimelineComponent() {
const boxRef = useRef(null);
const tlRef = useRef(null);
useEffect(() => {
tlRef.current = gsap.timeline();
tlRef.current
.to(boxRef.current, { x: 100, duration: 1 })
.to(boxRef.current, { y: 100, duration: 1 });
return () => {
if (tlRef.current) {
tlRef.current.kill();
}
};
}, []);
return <div ref={boxRef}>时间轴动画</div>;
}ScrollTrigger 清理
import { ScrollTrigger } from "gsap/ScrollTrigger";
function ScrollComponent() {
const boxRef = useRef(null);
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
gsap.to(boxRef.current, {
x: 100,
scrollTrigger: {
trigger: boxRef.current,
start: "top center"
}
});
return () => {
ScrollTrigger.getAll().forEach(st => st.kill());
};
}, []);
return <div ref={boxRef}>滚动动画</div>;
}🎬 实战案例
案例 1:列表项依次出现
function AnimatedList({ items }) {
const listRef = useRef(null);
useEffect(() => {
const items = listRef.current.querySelectorAll(".list-item");
gsap.from(items, {
opacity: 0,
y: 50,
duration: 0.5,
stagger: 0.1,
ease: "power2.out"
});
}, [items]);
return (
<ul ref={listRef}>
{items.map((item, i) => (
<li key={i} className="list-item">{item}</li>
))}
</ul>
);
}案例 2:卡片悬停效果
function HoverCard({ children }) {
const cardRef = useRef(null);
const handleMouseEnter = () => {
gsap.to(cardRef.current, {
scale: 1.05,
y: -10,
duration: 0.3,
ease: "power2.out"
});
};
const handleMouseLeave = () => {
gsap.to(cardRef.current, {
scale: 1,
y: 0,
duration: 0.3,
ease: "power2.in"
});
};
return (
<div
ref={cardRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</div>
);
}案例 3:模态框动画
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
const backdropRef = useRef(null);
useEffect(() => {
if (isOpen) {
// 显示动画
gsap.fromTo(backdropRef.current,
{ opacity: 0 },
{ opacity: 1, duration: 0.3 }
);
gsap.fromTo(modalRef.current,
{ opacity: 0, scale: 0.8, y: 50 },
{
opacity: 1,
scale: 1,
y: 0,
duration: 0.4,
ease: "back.out(1.7)"
}
);
} else {
// 隐藏动画
gsap.to(modalRef.current, {
opacity: 0,
scale: 0.8,
y: 50,
duration: 0.3
});
gsap.to(backdropRef.current, {
opacity: 0,
duration: 0.3
});
}
}, [isOpen]);
if (!isOpen) return null;
return (
<>
<div ref={backdropRef} className="modal-backdrop" onClick={onClose} />
<div ref={modalRef} className="modal">
{children}
</div>
</>
);
}案例 4:滚动触发动画
import { ScrollTrigger } from "gsap/ScrollTrigger";
function ScrollReveal({ children }) {
const elementRef = useRef(null);
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
gsap.from(elementRef.current, {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: elementRef.current,
start: "top 80%",
toggleActions: "play none none reverse"
}
});
return () => {
ScrollTrigger.getAll().forEach(st => st.kill());
};
}, []);
return <div ref={elementRef}>{children}</div>;
}案例 5:数字计数动画
function Counter({ target }) {
const counterRef = useRef(null);
useEffect(() => {
gsap.to(counterRef.current, {
textContent: target,
duration: 2,
snap: { textContent: 1 },
ease: "power2.out"
});
}, [target]);
return <span ref={counterRef}>0</span>;
}🛠️ 自定义 Hook
useGSAP Hook
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
function useGSAP(animationFn, deps = []) {
const elementRef = useRef(null);
const animationRef = useRef(null);
useEffect(() => {
if (elementRef.current) {
animationRef.current = animationFn(elementRef.current);
}
return () => {
if (animationRef.current) {
if (animationRef.current.kill) {
animationRef.current.kill();
} else if (Array.isArray(animationRef.current)) {
animationRef.current.forEach(anim => anim.kill());
}
}
};
}, deps);
return elementRef;
}
// 使用
function AnimatedBox() {
const boxRef = useGSAP((element) => {
return gsap.to(element, {
x: 100,
duration: 1
});
});
return <div ref={boxRef}>动画盒子</div>;
}useScrollTrigger Hook
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
function useScrollTrigger(animationFn, deps = []) {
const elementRef = useRef(null);
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
if (elementRef.current) {
animationFn(elementRef.current);
}
return () => {
ScrollTrigger.getAll().forEach(st => st.kill());
};
}, deps);
return elementRef;
}
// 使用
function ScrollReveal({ children }) {
const elementRef = useScrollTrigger((element) => {
gsap.from(element, {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: element,
start: "top 80%"
}
});
});
return <div ref={elementRef}>{children}</div>;
}💡 最佳实践
1. 使用 useRef 而不是 useState
// ✅ 推荐:使用 useRef
const boxRef = useRef(null);
// ❌ 不推荐:使用 useState
const [box, setBox] = useState(null);2. 在 useEffect 中初始化动画
// ✅ 推荐:组件挂载后初始化
useEffect(() => {
gsap.to(boxRef.current, { x: 100 });
}, []);
// ❌ 不推荐:在渲染中直接调用
gsap.to(boxRef.current, { x: 100 }); // 可能元素还未挂载3. 及时清理动画
useEffect(() => {
const animation = gsap.to(boxRef.current, { x: 100 });
return () => {
animation.kill(); // 清理动画
};
}, []);4. 避免在渲染中创建动画
// ❌ 不推荐:每次渲染都创建动画
function Component() {
gsap.to(".box", { x: 100 }); // 错误!
return <div className="box">盒子</div>;
}
// ✅ 推荐:在 useEffect 中创建
function Component() {
useEffect(() => {
gsap.to(".box", { x: 100 });
}, []);
return <div className="box">盒子</div>;
}