【react-spring】react-spring动画库学习教程(2025最新版)
学习上手react动画库react-spring这一篇全网最全教程就够了(2025最新版)
《react-spring 上手教程:用动画感动用户,用 Bug 感动自己》
🎬 react-spring 是 React 生态中最“魔性”的动画库之一。
它让你的 UI 动起来的同时,也让你的头发掉得更快。
🧠 一、安装启动指南(像恋爱一样简单,又像分手一样痛苦)
📦 安装方式
npm install react-spring
或使用 yarn:
yarn add react-spring
注意:
如果你是 React 18+ 用户,请确保版本 >= 9.x,否则你可能会看到类似下面的报错:
“You’re using a version older than the universe.”
💥 二、Hello World!动起来吧!
import { useSpring, animated } from 'react-spring';
function App() {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>我进场了,别鼓掌了</animated.div>;
}
你以为的效果:
元素优雅地淡入屏幕,用户感动落泪。
实际效果:
元素闪现一下就没了,控制台没报错但就是不执行动画。
解决方法:
检查是否用了 <div> 而不是 <animated.div>?
因为 react-spring 的动画只能作用于它自己的组件上!
🎯 三、10大业务场景(React + react-spring 的“高光时刻”)
| 场景 | 组件/Hook | 描述 |
|---|---|---|
| 页面进入动画 | useSpring |
页面加载时淡入、缩放 |
| 表单验证错误提示 | useTransition |
输入错误后弹出小红框 |
| 折叠面板展开收起 | useSpring + 条件渲染 |
控制高度变化动画 |
| 列表项增删动画 | useTransition |
列表项添加/删除时滑动进出 |
| 卡片翻转特效 | useSpring |
正反面切换动画 |
| 导航栏切换动画 | useChain |
多个动画按顺序播放 |
| 加载动画 | useSprings |
多个 loading 点点闪烁 |
| 工具提示 Tooltip 动画 | useTransition |
鼠标移入时渐显 |
| Tab 切换过渡 | useTransition |
Tab 内容切换时的位移动画 |
| 游戏界面转场 | useTrail |
多个元素依次出场 |
1️⃣ 页面进入动画:淡入 + 缩放
🧩 场景:
页面加载时使用 useSpring 实现元素从透明缩放为 0 到正常显示的入场动画。
✅ 正确代码:
import { useSpring, animated } from 'react-spring';
function Page() {
const props = useSpring({
from: { opacity: 0, transform: 'scale(0.8)' },
to: { opacity: 1, transform: 'scale(1)' },
config: { tension: 200, friction: 15 },
});
return (
<animated.div style={props} className="page-content">
页面内容
</animated.div>
);
}
⚠️ 注意事项:
- 使用
<animated.div>而非普通<div> - 动画属性建议使用
config自定义物理参数
2️⃣ 表单验证错误提示:输入错误后弹出小红框
🧩 场景:
当用户输入错误时,使用 useTransition 控制错误提示框的出现与消失。
✅ 正确代码:
import { useState } from 'react';
import { useTransition, animated } from 'react-spring';
function LoginForm() {
const [error, setError] = useState(false);
const transitions = useTransition(error, {
from: { opacity: 0, transform: 'translateY(-10px)' },
enter: { opacity: 1, transform: 'translateY(0px)' },
leave: { opacity: 0, transform: 'translateY(-10px)' },
});
const handleSubmit = () => {
if (/* 验证失败 */) {
setError(true);
setTimeout(() => setError(false), 2000);
}
};
return (
<form onSubmit={handleSubmit}>
{transitions((style) =>
error ? <animated.div style={style} className="error-box">请输入正确格式</animated.div> : null
)}
<input type="text" />
<button type="submit">提交</button>
</form>
);
}
⚠️ 注意事项:
useTransition的依赖项要传对(如[error])- 可以控制多个状态下的动画表现
3️⃣ 折叠面板展开收起:高度变化动画
🧩 场景:
点击按钮控制面板展开/收起,使用 useSpring 实现高度渐变动画。
✅ 正确代码:
import { useState } from 'react';
import { useSpring, animated } from 'react-spring';
function CollapsePanel() {
const [open, setOpen] = useState(false);
const props = useSpring({
height: open ? 200 : 0,
opacity: open ? 1 : 0,
overflow: 'hidden',
});
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
<animated.div style={props} className="panel-content">
这是折叠面板的内容
</animated.div>
</div>
);
}
⚠️ 注意事项:
- 设置
overflow: hidden防止内容溢出 - 动画过程可加入
config调整流畅度
4️⃣ 列表项增删动画:滑动进出
🧩 场景:
使用 useTransition 实现列表项添加和删除时的滑动动画。
✅ 正确代码:
import { useState } from 'react';
import { useTransition, animated } from 'react-spring';
function TodoList() {
const [items, setItems] = useState(['苹果', '香蕉']);
const transitions = useTransition(items, {
keys: items,
from: { transform: 'translateX(100%)', opacity: 0 },
enter: { transform: 'translateX(0)', opacity: 1 },
leave: { transform: 'translateX(-100%)', opacity: 0 },
});
return (
<div>
{transitions((style, item) => (
<animated.div style={style}>{item}</animated.div>
))}
<button onClick={() => setItems([...items, '西瓜'])}>添加</button>
</div>
);
}
⚠️ 注意事项:
- 必须设置
keys属性 - 添加和删除动画通过
from/enter/leave控制
5️⃣ 卡片翻转特效:正反面切换动画
🧩 场景:
点击卡片触发翻转动画,展示正面和背面信息。
✅ 正确代码:
import { useState } from 'react';
import { useSpring, animated } from 'react-spring';
function FlipCard() {
const [flipped, setFlipped] = useState(false);
const { transform } = useSpring({
transform: `perspective(600px) rotateY(${flipped ? 180 : 0}deg)`,
config: { mass: 5, tension: 500, friction: 80 },
});
return (
<div onClick={() => setFlipped(!flipped)} style={{ cursor: 'pointer' }}>
<animated.div
style={{
transform,
backfaceVisibility: 'hidden',
position: 'absolute',
}}
>
正面内容
</animated.div>
<animated.div
style={{
transform,
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
背面内容
</animated.div>
</div>
);
}
⚠️ 注意事项:
- 使用
backfaceVisibility防止双面重叠 - 翻转角度需同步控制两面动画
6️⃣ 导航栏切换动画:多个动画顺序播放
🧩 场景:
多个动画按顺序播放,例如左侧菜单先出现,右侧内容再出现。
✅ 正确代码:
import { useChain, useSpringRef } from 'react-spring';
function SequentialAnimation() {
const ref1 = useSpringRef();
const ref2 = useSpringRef();
const spring1 = useSpring({ ref: ref1, from: { x: -100 }, to: { x: 0 } });
const spring2 = useSpring({ ref: ref2, from: { x: 100 }, to: { x: 0 } });
useChain([ref1, ref2], [0, 0.5]); // 第二个动画延迟 0.5s
return (
<>
<animated.div style={{ transform: spring1.x.to(x => `translateX(${x}px)`) }}>左侧菜单</animated.div>
<animated.div style={{ transform: spring2.x.to(x => `translateX(${x}px)`) }}>右侧内容</animated.div>
</>
);
}
⚠️ 注意事项:
- 多动画控制必须使用
useChain - 可以通过数组控制播放顺序和延迟
7️⃣ 加载动画:多个点点闪烁动画
🧩 场景:
使用 useSprings 实现多个 loading 点依次闪烁效果。
✅ 正确代码:
import { useSprings } from 'react-spring';
function LoadingDots() {
const springs = useSprings(
3,
Array(3).fill({
from: { opacity: 0.4 },
to: async (next) => {
while (true) {
await next({ opacity: 1 });
await next({ opacity: 0.4 });
}
},
})
);
return (
<div style={{ display: 'flex' }}>
{springs.map((props, i) => (
<animated.div key={i} style={{ ...props, width: 10, height: 10, borderRadius: '50%', margin: 5, background: '#000' }} />
))}
</div>
);
}
⚠️ 注意事项:
- 使用
async函数实现无限循环动画 - 可用于加载中、等待状态提示
8️⃣ Tooltip 工具提示动画:鼠标移入渐显
🧩 场景:
鼠标移入时,Tooltip 渐显并带轻微位移动画。
✅ 正确代码:
import { useState } from 'react';
import { useTransition, animated } from 'react-spring';
function Tooltip({ children }) {
const [show, setShow] = useState(false);
const transitions = useTransition(show, {
from: { opacity: 0, transform: 'translateY(-10px)' },
enter: { opacity: 1, transform: 'translateY(0)' },
leave: { opacity: 0, transform: 'translateY(-10px)' },
});
return (
<div onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
{children}
{transitions((style) =>
show ? (
<animated.div style={style} className="tooltip">
我是提示内容
</animated.div>
) : null
)}
</div>
);
}
⚠️ 注意事项:
- 使用
onMouseEnter和onMouseLeave控制状态 - 可结合 CSS 定位实现更复杂的 tooltip 样式
9️⃣ Tab 切换过渡动画:内容切换位移动画
🧩 场景:
Tab 切换时,使用 useTransition 实现内容从左到右滑动切换动画。
✅ 正确代码:
import { useState } from 'react';
import { useTransition, animated } from 'react-spring';
function TabView() {
const [index, setIndex] = useState(0);
const tabs = ['首页', '订单', '我的'];
const transition = useTransition(index, {
from: { transform: index === 0 ? 'translateX(100%)' : 'translateX(-100%)', opacity: 0 },
enter: { transform: 'translateX(0)', opacity: 1 },
leave: { position: 'absolute' },
});
return (
<div>
<div style={{ display: 'flex' }}>
{tabs.map((tab, i) => (
<button key={i} onClick={() => setIndex(i)}>{tab}</button>
))}
</div>
{transition((style) => (
<animated.div style={style} className="tab-content">
{tabs[index]}
</animated.div>
))}
</div>
);
}
⚠️ 注意事项:
leave中使用position: absolute防止布局抖动- 可根据当前索引方向控制动画方向
🔟 游戏界面转场动画:多个元素依次出场
🧩 场景:
游戏开始或关卡切换时,多个元素依次出场。
✅ 正确代码:
import { useTrail } from 'react-spring';
function GameStartScreen() {
const items = ['角色', '武器', '敌人', '背景'];
const trail = useTrail(items.length, {
from: { opacity: 0, transform: 'translateY(50px)' },
to: { opacity: 1, transform: 'translateY(0)' },
});
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{trail.map(({ ...rest }, index) => (
<animated.div key={index} style={rest}>
{items[index]}
</animated.div>
))}
</div>
);
}
⚠️ 注意事项:
useTrail会自动计算每个元素的动画时间差- 可用于开场动画、元素依次展示等场景
🐞 四、10个常见 Bug & Issue(附解决方案 + 吐槽)
| Bug 描述 | 原因 | 解决方案 | 吐槽 |
|---|---|---|---|
| 动画不执行 | 使用了普通 div 而非 animated.div | 改成 <animated.div> |
这种限制太任性了吧? |
| 动画一开始有抖动 | 初始值未设置正确 | 设置 from 属性 |
初心不能忘啊! |
| 列表动画卡顿 | key 不唯一 | 使用唯一 ID 作为 key | key 是灵魂,灵魂不能重复 |
| useTransition 添加 item 时动画不触发 | 没有监听 item 数组变化 | 使用 deps 或 useEffect 触发更新 | 动画比对象还难搞 |
| 动画结束后状态丢失 | 没有调用 onRest | 在 onRest 中更新状态 | 我要你停下来的时候告诉我一声行不行? |
| 多个动画同时播放混乱 | 没有用 useChain | 使用 useChain([ref1, ref2]) 控制顺序 |
谁说动画也要讲先后顺序 |
| 自定义组件无法绑定动画属性 | 没有 forwardRef | 使用 forwardRef 包裹组件 |
react-spring 对组件太挑食 |
| TypeScript 类型报错 | 缺少类型定义 | 手动声明 props 类型或使用泛型 | 类型系统也能被它整崩溃 |
| 动画在 SSR 中失效 | DOM 不存在 | 使用 useEffect 控制首次动画 | 服务端没有 DOM,动画去哪演? |
| 动画性能差 | 渲染过多 | 使用 shouldDepthTest 或 useMemo | 你是想做动画还是跑游戏? |
🐛 Bug 1:动画根本不动?!(No animation at all)
❌ 错误代码:
import { useSpring } from 'react-spring';
function App() {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <div style={props}>我应该淡入</div>; // ❌ 使用了普通 div 而非 animated.div
}
📦 报错信息:
控制台无报错,但动画没执行。
✅ 正确写法:
import { useSpring, animated } from 'react-spring';
function App() {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>我终于动起来了!</animated.div>;
}
⚠️ 注意事项:
style属性必须绑定到<animated.xxx>组件上。- 普通 HTML 元素不支持 react-spring 的插值系统。
🐛 Bug 2:动画一开始抖动一下?(Initial jank or flicker)
❌ 错误代码:
const props = useSpring({
to: { opacity: 1 },
});
📦 报错信息:
页面加载瞬间闪现透明度为 1 的状态,然后又消失再出现。
✅ 正确写法:
const props = useSpring({
from: { opacity: 0 },
to: { opacity: 1 },
});
⚠️ 注意事项:
- 如果没有设置
from,初始值默认为当前 DOM 状态,导致动画起点混乱。 - 动画应始终从明确的初始状态开始。
🐛 Bug 3:列表动画卡顿?(List animation laggy)
❌ 错误代码:
import { useState } from 'react';
import { useTransition } from 'react-spring';
function ListComponent() {
const [items, setItems] = useState(['苹果', '香蕉']);
const transitions = useTransition(items, {
keys: items,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
return (
<div>
{transitions((style) => (
<animated.div style={style}>{item}</animated.div>
))}
<button onClick={() => setItems([...items, '西瓜'])}>添加水果</button>
</div>
);
}
📦 报错信息:
列表项多时动画卡顿严重。
✅ 正确写法:
// ✅ 使用唯一 key,并避免不必要的重渲染
function ListComponent() {
const [items, setItems] = useState([{ id: 1, text: '苹果' }, { id: 2, text: '香蕉' }]);
const nextId = useRef(3);
const transitions = useTransition(items, {
keys: (item) => item.id,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
return (
<div>
{transitions((style, item) => (
<animated.div style={style}>{item.text}</animated.div>
))}
<button onClick={() =>
setItems([...items, { id: nextId.current++, text: '西瓜' }])
}>
添加水果
</button>
</div>
);
}
⚠️ 注意事项:
- 列表动画建议使用唯一
key - 避免频繁 re-render,可使用
React.memo或useCallback
🐛 Bug 4:useTransition 添加 item 时动画不触发?
❌ 错误代码:
const transitions = useTransition(items, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
📦 报错信息:
新增 item 时不触发 enter 动画。
✅ 正确写法:
const transitions = useTransition(items, {
keys: (item) => item.id,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
⚠️ 注意事项:
- 必须指定
keys,否则无法识别新增/删除项 - 推荐使用唯一 ID 作为 key
🐛 Bug 5:动画结束后状态丢失?(Animation resets after finish)
❌ 错误代码:
const props = useSpring({
from: { scale: 0 },
to: { scale: 1 },
onRest: () => {
console.log('动画结束');
},
});
📦 报错信息:
动画结束后缩放变回 0?
✅ 正确写法:
const [show, setShow] = useState(true);
const props = useSpring({
from: { scale: 0 },
to: { scale: show ? 1 : 0 },
onRest: () => {
if (!show) {
console.log('已经隐藏');
}
},
});
⚠️ 注意事项:
- 单次动画不会保留最终状态,需用状态驱动动画变化
- 可用于控制组件显示/隐藏后的清理逻辑
🐛 Bug 6:多个动画同时播放乱套?(Multiple animations play out of order)
❌ 错误代码:
const first = useSpring({ x: 100 });
const second = useSpring({ y: 100 });
return (
<>
<animated.div style={{ transform: first.x.to(x => `translateX(${x}px)`) }} />
<animated.div style={{ transform: second.y.to(y => `translateY(${y}px)`) }} />
</>
);
📦 报错信息:
两个动画几乎同时开始,顺序不可控。
✅ 正确写法:
import { useChain, useSpringRef } from 'react-spring';
function SequentialAnimation() {
const ref1 = useSpringRef();
const ref2 = useSpringRef();
const spring1 = useSpring({ ref: ref1, from: { x: 0 }, to: { x: 100 } });
const spring2 = useSpring({ ref: ref2, from: { y: 0 }, to: { y: 100 } });
useChain([ref1, ref2], [0, 1]); // 第二个动画延迟 1s
return (
<>
<animated.div style={{ transform: spring1.x.to(x => `translateX(${x}px)`) }} />
<animated.div style={{ transform: spring2.y.to(y => `translateY(${y}px)`) }} />
</>
);
}
⚠️ 注意事项:
- 默认并行执行所有动画
- 如需顺序控制,必须使用
useChain
🐛 Bug 7:自定义组件无法绑定动画属性?(Custom component not animating)
❌ 错误代码:
const MyBox = ({ style }) => <div style={style}>我是盒子</div>;
function App() {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <MyBox style={props} />;
}
📦 报错信息:
没有错误,但动画无效。
✅ 正确写法:
const MyBox = React.forwardRef(({ style }, ref) => (
<animated.div ref={ref} style={style}>
我是盒子
</animated.div>
));
function App() {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <MyBox style={props} />;
}
⚠️ 注意事项:
- 自定义组件必须 forwardRef 并使用 animated 包裹
- 否则无法接收动画属性
🐛 Bug 8:TypeScript 类型报错?(TypeScript complains about style)
❌ 错误代码:
interface BoxProps {
style?: object;
}
const Box: React.FC<BoxProps> = ({ style }) => (
<div style={style}>盒子</div>
);
📦 报错信息:
Type ‘{ opacity: number; }’ is missing the following properties from type ‘CSSProperties’: transform…
✅ 正确写法:
import { CSSProperties } from 'react';
import { AnimatedStyle } from 'react-spring';
interface BoxProps {
style?: AnimatedStyle;
}
⚠️ 注意事项:
AnimatedStyle是 react-spring 特有的类型- 不要混用普通
CSSProperties
🐛 Bug 9:SSR 中动画失效?(Server Side Rendering)
❌ 错误代码:
const props = useSpring({ opacity: 1 });
📦 报错信息:
ReferenceError: window is not defined
✅ 正确写法:
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
const DynamicAnimatedComponent = dynamic(() => import('../components/AnimatedBox'), {
ssr: false,
});
function Page() {
return <DynamicAnimatedComponent />;
}
⚠️ 注意事项:
- react-spring 依赖浏览器 API(如 requestAnimationFrame)
- SSR 中应动态导入动画组件并禁用服务端渲染
🐛 Bug 10:动画性能差?(Performance issues)
❌ 错误代码:
const springs = useSprings(
items.length,
items.map(() => ({ from: { opacity: 0 }, to: { opacity: 1 } }))
);
📦 报错信息:
列表项多的时候动画卡顿严重。
✅ 正确写法:
const springs = useSprings(
items.length,
items.map(() => ({
from: { opacity: 0 },
to: { opacity: 1 },
config: { duration: 300 }, // 缩短动画时间
})),
[items]
);
⚠️ 注意事项:
- 设置合理的
config时间参数 - 添加
deps防止重复计算
💬 五、10道高频面试题(假装你在 BAT 面试)
| 题目 | 参考答案 |
|---|---|
| 1. react-spring 和 Framer Motion 的区别是什么? | react-spring 更底层灵活,Framer Motion 更简洁易用 |
| 2. 如何实现一个列表项的添加删除动画? | 使用 useTransition,根据 key 变化触发动画 |
| 3. 如何让多个动画按顺序播放? | 使用 useChain + refs 控制播放顺序 |
| 4. useSpring 和 useTransition 的区别是什么? | Spring 是物理模拟,Transition 是条件切换动画 |
| 5. 如何在 SSR 中安全使用 react-spring? | 在 useEffect 中初始化动画,避免首屏直接执行 |
| 6. 如何自定义弹簧参数? | 使用 config 属性,如 config: { tension: 200, friction: 20 } |
| 7. 如何暂停和恢复动画? | 使用 pause 属性控制动画状态 |
| 8. 如何在组件卸载时触发动画? | 使用 leave 配置项,在 Transition 中定义 |
| 9. 如何实现拖拽交互与动画联动? | 使用 useDrag + useSpring 联合操控位置 |
| 10. 如何优化 react-spring 的性能? | 避免频繁重渲染,使用 memo、shouldDepthTest、独立动画配置等手段 |
🤯 六、react-spring 的哲学观(开发者的内心独白)
-
“我本将心向明月,奈何明月照沟渠。”
→ 明月是动画效果,沟渠是你写的代码。 -
“我写了一个很酷的动画,上线后用户说眼花。”
→ 很多时候,用户体验比技术炫技更重要。 -
“我以为是 bug,其实是 API 设计哲学。”
→ react-spring 的 API 设计像是程序员的修罗场。 -
“文档写得像谜语,源码读得像天书。”
→ 开源项目有时候就像一场大型猜谜游戏。
🎉 七、总结一段话
react-spring 是一个“强大但调皮”的动画库。
它能让你的页面看起来像迪士尼动画,也能让你的头发像秋天的落叶一样掉落。
用得好,用户以为你是个艺术家;用不好,你可能连编译都过不了。
所以,记住一句话:
“你可以不用 react-spring,但你必须知道 react-spring。”
🎉 完 🎉
更多推荐
所有评论(0)