react-spring 安装与使用详解
react-spring 是一个基于**弹簧物理(spring physics)**的 React 动画库。和 CSS transition 那种"固定时长、固定缓动"的动画不同,它用弹簧模型驱动动画:元素会像真实弹簧一样,带着惯性、过冲和回弹地运动,动画感觉更自然、更有"肉感"。
目录
- 安装
- 核心概念:什么是弹簧动画
- useSpring:单值弹簧动画
- useSprings:批量弹簧动画
- useTransition:进入/离开动画
- useTrail:列表尾随动画
- useChain:串联多个动画
- animated 组件:把动画值绑定到 DOM
- config:弹簧参数调优
- 常见实战场景
- 注意事项与常见坑
1. 安装
npm install @react-spring/web
# 或
pnpm add @react-spring/web
# 或
yarn add @react-spring/web注意:
- 用的是
@react-spring/web(v9+ 的包名),不是老的react-spring。老包react-spring已停止维护,别装错。 @react-spring/web是 Web 平台的包;如果做 React Native,装@react-spring/native。- 不需要额外的类型包,自带 TypeScript 类型。
// 所有 API 都从这个包导入
import { useSpring, animated } from "@react-spring/web";2. 核心概念:什么是弹簧动画
传统 CSS 动画:
/* 固定 0.3 秒、固定缓动曲线,时间到就停 */
transition: all 0.3s ease;react-spring 的弹簧动画:
- 你不指定时长,而是告诉它弹簧的物理参数:质量(mass)、张力/刚度(tension)、摩擦力(friction)。
- 元素从当前值"弹"向目标值,中间可能冲过头再回来(overshoot),最终自然停稳。
- 就像拉一根橡皮筋再松手:它会弹回去,还可能来回晃几下,而不是匀速移动。
这种动画的好处是:动画中途可以随时改变目标值,它会从当前位置平滑地接着弹,不会像 CSS transition 那样突然"跳"或重启。
3. useSpring:单值弹簧动画
useSpring 用于一个组件的一组动画值。返回一个包含动画值的对象,配合 animated 组件使用。
3.1 最基础用法:数值弹簧
import { useState } from "react";
import { useSpring, animated } from "@react-spring/web";
export default function Counter() {
const [open, setOpen] = useState(false);
// open 变化时,opacity 和 scale 会从当前值弹向目标值
const springs = useSpring({
opacity: open ? 1 : 0,
scale: open ? 1 : 0.8,
});
return (
<div>
<button onClick={() => setOpen(!open)}>切换</button>
{/* animated.div 是能接收动画值的 div */}
<animated.div
style={{
opacity: springs.opacity,
transform: springs.scale.to((s) => `scale(${s})`),
}}
>
我会弹簧般地出现和消失
</animated.div>
</div>
);
}3.2 from / to 写法
可以明确指定起点和终点:
const springs = useSpring({
from: { x: 0, opacity: 0 }, // 初始值
to: { x: 100, opacity: 1 }, // 目标值
});3.3 用 api 手动控制(更灵活)
useSpring 第二个返回值是 api,可以手动 start 动画,适合事件驱动:
import { useSpring, animated } from "@react-spring/web";
export default function Box() {
const [springs, api] = useSpring(() => ({
x: 0,
opacity: 1,
}));
return (
<div>
<animated.div
onClick={() => {
// 点击时从当前值弹到 x: 200
api.start({ x: 200 });
}}
style={{
width: 80,
height: 80,
background: "#1677ff",
x: springs.x, // 注意:x 是 transform 的简写
}}
/>
<button onClick={() => api.start({ x: 0, opacity: 1 })}>重置</button>
</div>
);
}x / y / scale 等简写:react-spring 内置支持一些 transform 简写,直接传动画值即可,不用手动拼 transform 字符串。
4. useSprings:批量弹簧动画
当需要对一组数量确定的元素做动画时,用 useSprings。第一个参数是数量,第二个参数是生成每一项动画配置的函数。
import { useSprings, animated } from "@react-spring/web";
const items = ["苹果", "香蕉", "橙子", "葡萄"];
export default function List() {
const springs = useSprings(
items.length,
items.map((_, index) => ({
from: { opacity: 0, x: -20 },
to: { opacity: 1, x: 0 },
delay: index * 100, // 每一项延迟依次出现
})),
);
return (
<div>
{springs.map((style, i) => (
<animated.div key={items[i]} style={style}>
{items[i]}
</animated.div>
))}
</div>
);
}5. useTransition:进入/离开动画
useTransition 专门处理元素挂载/卸载时的进入和离开动画,比如条件渲染、列表增删。它会返回过渡中的每一项及其动画样式。
5.1 条件渲染的淡入淡出
import { useState } from "react";
import { useTransition, animated } from "@react-spring/web";
export default function ToggleBox() {
const [show, setShow] = useState(false);
const transitions = useTransition(show, {
from: { opacity: 0, transform: "translateY(20px)" },
enter: { opacity: 1, transform: "translateY(0px)" },
leave: { opacity: 0, transform: "translateY(-20px)" },
});
return (
<div>
<button onClick={() => setShow(!show)}>切换</button>
{/* transitions 返回一个函数,show 为 true 时渲染内容 */}
{transitions((style, item) =>
item ? (
<animated.div style={style}>
我进场时淡入上移,离场时淡出下移
</animated.div>
) : null,
)}
</div>
);
}5.2 列表增删动画
import { useState } from "react";
import { useTransition, animated } from "@react-spring/web";
let id = 3;
export default function TodoList() {
const [items, setItems] = useState([
{ id: 1, text: "学习 react-spring" },
{ id: 2, text: "写动画示例" },
]);
const transitions = useTransition(items, {
keys: (item) => item.id, // 用 id 做 key,才能识别增删
from: { opacity: 0, height: 0 },
enter: { opacity: 1, height: 40 },
leave: { opacity: 0, height: 0 },
});
return (
<div>
<button
onClick={() => setItems([...items, { id: id++, text: `新任务 ${id}` }])}
>
添加
</button>
{transitions((style, item) => (
<animated.div
style={{ ...style, overflow: "hidden", lineHeight: "40px" }}
>
{item.text}
<button
onClick={() => setItems(items.filter((i) => i.id !== item.id))}
>
删除
</button>
</animated.div>
))}
</div>
);
}6. useTrail:列表尾随动画
useTrail 让一组元素一个接一个地动画,像队伍依次跟上,适合做列表入场、逐个显示的效果。
import { useTrail, animated } from "@react-spring/web";
const tags = ["React", "动画", "弹簧", "物理", "自然"];
export default function TagList() {
const trail = useTrail(tags.length, {
from: { opacity: 0, transform: "translateX(-20px)" },
to: { opacity: 1, transform: "translateX(0px)" },
config: { tension: 220, friction: 20 },
});
return (
<div style={{ display: "flex", gap: 8 }}>
{trail.map((style, i) => (
<animated.span key={tags[i]} style={style}>
{tags[i]}
</animated.span>
))}
</div>
);
}useTrail vs useSprings + delay:
useTrail是前一项动画带动后一项,节奏更自然连贯。useSprings + delay是各项各自延时启动,机械感更强。- 列表入场优先用
useTrail。
7. useChain:串联多个动画
useChain 把多个 spring 按顺序串联:前一个动画播完(或播到某个时间点)再开始下一个。适合做"先展开容器,再逐个出现内容"这类分步动画。
import { useRef } from "react";
import { useSpring, useTrail, useChain, animated } from "@react-spring/web";
const items = ["第一步", "第二步", "第三步"];
export default function Stepper() {
const springRef = useRef(null);
const trailRef = useRef(null);
// 1. 先定义容器动画
const containerSpring = useSpring({
ref: springRef, // 把 ref 挂上去,供 useChain 调度
from: { width: 0, opacity: 0 },
to: { width: 300, opacity: 1 },
});
// 2. 再定义列表项动画
const trail = useTrail(items.length, {
ref: trailRef,
from: { opacity: 0, x: -10 },
to: { opacity: 1, x: 0 },
});
// 3. 串联:先容器,后列表项(0.3 表示列表在容器动画播到 30% 时就开始)
useChain([springRef, trailRef], [0, 0.3]);
return (
<animated.div style={{ ...containerSpring, overflow: "hidden" }}>
{trail.map((style, i) => (
<animated.div key={items[i]} style={style}>
{items[i]}
</animated.div>
))}
</animated.div>
);
}8. animated 组件:把动画值绑定到 DOM
useSpring 返回的是动画值对象(AnimatedValue),不能直接塞给普通 DOM 元素的 style,必须用 animated 包装过的组件。
import { animated } from "@react-spring/web";
// 内置标签都有对应的 animated 版本
<animated.div style={springs} />
<animated.span style={springs} />
<animated.button style={springs} />
// 也可以包装自定义组件(该组件需要把 style 透传到 DOM)
const AnimatedCard = animated(MyCard);
<AnimatedCard style={springs} />动画值转换(to / interpolate):把数值映射成任意样式字符串。
const springs = useSpring({ progress: open ? 1 : 0 });
<animated.div
style={{
// 把 0~1 映射成 translateX 和旋转
transform: springs.progress.to(
(p) => `translateX(${p * 100}px) rotate(${p * 360}deg)`,
),
// 把 0~1 映射成背景色
background: springs.progress.to({
range: [0, 1],
output: ["#ff4d4f", "#52c41a"],
}),
}}
/>;旧版本用
interpolate,新版本推荐.to(),含义相同。
9. config:弹簧参数调优
config 是弹簧动画的灵魂,控制动画的"手感"。核心参数:
| 参数 | 含义 | 调大的效果 |
|---|---|---|
mass | 质量 | 更重,运动更迟缓、惯性更大 |
tension | 张力/刚度 | 更有劲,运动更快、回弹更猛 |
friction | 摩擦力 | 阻尼更大,晃动更快停止 |
clamp | 是否夹紧 | 设为 true 时禁止过冲,直接停稳 |
内置了几套预设:
import { config } from "@react-spring/web";
const springs = useSpring({
to: { x: 100 },
// 预设:default / gentle / wobbly / stiff / slow / molasses
config: config.wobbly, // 会明显晃动,弹性十足
});
// 也可以自定义
const springs2 = useSpring({
to: { x: 100 },
config: { mass: 1, tension: 280, friction: 60 }, // 官方默认值
});常用调参组合:
- 轻柔自然:
config.gentle或{ tension: 120, friction: 14 } - 弹跳明显:
config.wobbly或{ tension: 180, friction: 12 } - 干脆利落(不过冲):
{ tension: 300, friction: 30, clamp: true }
10. 常见实战场景
10.1 鼠标跟随的元素
import { useSpring, animated } from "@react-spring/web";
export default function Follower() {
const [springs, api] = useSpring(() => ({ x: 0, y: 0 }));
return (
<div
style={{ height: "100vh", position: "relative" }}
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
api.start({
x: e.clientX - rect.left - 25,
y: e.clientY - rect.top - 25,
});
}}
>
<animated.div
style={{
position: "absolute",
width: 50,
height: 50,
borderRadius: "50%",
background: "#1677ff",
x: springs.x,
y: springs.y,
}}
/>
</div>
);
}鼠标移动时元素会持续跟踪,但因为弹簧惯性,会有"慢半拍再追上来"的丝滑感,这是 CSS transition 做不出来的。
10.2 滚动数字
import { useSpring, animated } from "@react-spring/web";
export default function NumberScroll({ value }: { value: number }) {
const { number } = useSpring({
from: { number: 0 },
to: { number: value },
config: config.molasses, // 缓慢滚动的效果
});
// number 是动画值,渲染时取当前值并保留整数
return <animated.span>{number.to((n) => n.toFixed(0))}</animated.span>;
}10.3 手风琴展开收起
import { useState } from "react";
import { useSpring, animated } from "@react-spring/web";
export default function Accordion() {
const [open, setOpen] = useState(false);
const springs = useSpring({
height: open ? 200 : 0,
opacity: open ? 1 : 0,
});
return (
<div>
<button onClick={() => setOpen(!open)}>展开/收起</button>
<animated.div style={{ ...springs, overflow: "hidden" }}>
<div style={{ padding: 20 }}>这里是可折叠的内容区域</div>
</animated.div>
</div>
);
}11. 注意事项与常见坑
11.1 必须用 animated 组件接收动画值
// ❌ 错误:普通 div 无法识别动画值对象
<div style={{ opacity: springs.opacity }} />
// ✅ 正确:用 animated.div
<animated.div style={{ opacity: springs.opacity }} />11.2 依赖数组与 stale closure
useSpring(() => ({...})) 的工厂函数只在初始化时执行一次。如果动画依赖外部状态,要么把它放进 to 的对象写法(自动响应),要么显式传依赖:
// ✅ 推荐:对象写法,open 变化自动触发动画
const springs = useSpring({ x: open ? 100 : 0 });
// ✅ 或者:工厂写法 + 依赖数组
const [springs, api] = useSpring(
() => ({ x: open ? 100 : 0 }),
[open], // open 变化时重新评估
);11.3 列表动画一定要给稳定的 key
useTransition 处理列表增删时,必须提供 keys 配置,且 key 要稳定唯一,否则无法正确识别"哪个是新来的、哪个是要走的",动画会错乱。
useTransition(items, {
keys: (item) => item.id, // ✅ 用唯一 id,不要用数组下标
// ...
});11.4 性能:优先动画 transform 和 opacity
弹簧动画每帧都在计算并更新值,开销不小。尽量只动画 transform(x/y/scale/rotate)和 opacity,这两个属性可以走 GPU 合成,性能好。避免频繁动画 width/height/top/left 等会触发重排的属性。
// ✅ 好:用 transform 移动
<animated.div style={{ x: springs.x }} />
// ⚠️ 慎用:用 left 移动,每帧都触发 layout
<animated.div style={{ left: springs.x.to((v) => `${v}px`) }} />11.5 不要在动画值上做复杂逻辑
动画值(springs.x 这类)是特殊对象,不是普通数字。想参与计算要用 .to() 做映射,不要直接 springs.x + 10。
// ❌ 错误
const doubled = springs.x + 10;
// ✅ 正确
transform: springs.x.to((x) => `translateX(${x * 2}px)`);11.6 组件卸载时清理
useSpring 等 hook 在组件卸载时会自动清理,不用手动取消。但如果你在 useEffect 里手动 api.start() 了一个无限循环动画,要注意卸载时停止:
useEffect(() => {
const result = api.start({
to: { rotate: 360 },
loop: true, // 无限循环
});
return () => result.stop(); // 卸载时停止
}, [api]);11.7 SSR / 服务端渲染
动画依赖浏览器环境。SSR 场景(如 Next.js)中,含动画的组件建议只在客户端渲染(dynamic(() => import(...), { ssr: false })),避免 hydration 不一致。
小结
- 装包:
@react-spring/web,不是老的react-spring。 - 核心思路:不指定时长,用弹簧物理参数(mass/tension/friction)驱动动画。
- 四大 hook:
useSpring(单值)、useSprings(批量)、useTransition(进出)、useTrail(尾随),useChain串联它们。 - 绑定 DOM:动画值必须传给
animated.xxx组件,复杂样式用.to()映射。 - 性能:优先动画
transform和opacity,列表记得给稳定 key。