Skip to content

react-spring 安装与使用详解 ​

react-spring 是一个基于**弹簧物理(spring physics)**的 React 动画库。和 CSS transition 那种"固定时长、固定缓动"的动画不同,它用弹簧模型驱动动画:元素会像真实弹簧一样,带着惯性、过冲和回弹地运动,动画感觉更自然、更有"肉感"。


目录 ​

  1. 安装
  2. 核心概念:什么是弹簧动画
  3. useSpring:单值弹簧动画
  4. useSprings:批量弹簧动画
  5. useTransition:进入/离开动画
  6. useTrail:列表尾随动画
  7. useChain:串联多个动画
  8. animated 组件:把动画值绑定到 DOM
  9. config:弹簧参数调优
  10. 常见实战场景
  11. 注意事项与常见坑

1. 安装 ​

bash
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 类型。
tsx
// 所有 API 都从这个包导入
import { useSpring, animated } from "@react-spring/web";

2. 核心概念:什么是弹簧动画 ​

传统 CSS 动画:

css
/* 固定 0.3 秒、固定缓动曲线,时间到就停 */
transition: all 0.3s ease;

react-spring 的弹簧动画:

  • 你不指定时长,而是告诉它弹簧的物理参数:质量(mass)、张力/刚度(tension)、摩擦力(friction)。
  • 元素从当前值"弹"向目标值,中间可能冲过头再回来(overshoot),最终自然停稳。
  • 就像拉一根橡皮筋再松手:它会弹回去,还可能来回晃几下,而不是匀速移动。

这种动画的好处是:动画中途可以随时改变目标值,它会从当前位置平滑地接着弹,不会像 CSS transition 那样突然"跳"或重启。


3. useSpring:单值弹簧动画 ​

useSpring 用于一个组件的一组动画值。返回一个包含动画值的对象,配合 animated 组件使用。

3.1 最基础用法:数值弹簧 ​

tsx
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 写法 ​

可以明确指定起点和终点:

tsx
const springs = useSpring({
  from: { x: 0, opacity: 0 }, // 初始值
  to: { x: 100, opacity: 1 }, // 目标值
});

3.3 用 api 手动控制(更灵活) ​

useSpring 第二个返回值是 api,可以手动 start 动画,适合事件驱动:

tsx
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。第一个参数是数量,第二个参数是生成每一项动画配置的函数。

tsx
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 条件渲染的淡入淡出 ​

tsx
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 列表增删动画 ​

tsx
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 让一组元素一个接一个地动画,像队伍依次跟上,适合做列表入场、逐个显示的效果。

tsx
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 按顺序串联:前一个动画播完(或播到某个时间点)再开始下一个。适合做"先展开容器,再逐个出现内容"这类分步动画。

tsx
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 包装过的组件。

tsx
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):把数值映射成任意样式字符串。

tsx
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 时禁止过冲,直接停稳

内置了几套预设:

tsx
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 鼠标跟随的元素 ​

tsx
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 滚动数字 ​

tsx
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 手风琴展开收起 ​

tsx
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 组件接收动画值 ​

tsx
// ❌ 错误:普通 div 无法识别动画值对象
<div style={{ opacity: springs.opacity }} />

// ✅ 正确:用 animated.div
<animated.div style={{ opacity: springs.opacity }} />

11.2 依赖数组与 stale closure ​

useSpring(() => ({...})) 的工厂函数只在初始化时执行一次。如果动画依赖外部状态,要么把它放进 to 的对象写法(自动响应),要么显式传依赖:

tsx
// ✅ 推荐:对象写法,open 变化自动触发动画
const springs = useSpring({ x: open ? 100 : 0 });

// ✅ 或者:工厂写法 + 依赖数组
const [springs, api] = useSpring(
  () => ({ x: open ? 100 : 0 }),
  [open], // open 变化时重新评估
);

11.3 列表动画一定要给稳定的 key ​

useTransition 处理列表增删时,必须提供 keys 配置,且 key 要稳定唯一,否则无法正确识别"哪个是新来的、哪个是要走的",动画会错乱。

tsx
useTransition(items, {
  keys: (item) => item.id, // ✅ 用唯一 id,不要用数组下标
  // ...
});

11.4 性能:优先动画 transform 和 opacity ​

弹簧动画每帧都在计算并更新值,开销不小。尽量只动画 transform(x/y/scale/rotate)和 opacity,这两个属性可以走 GPU 合成,性能好。避免频繁动画 width/height/top/left 等会触发重排的属性。

tsx
// ✅ 好:用 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。

tsx
// ❌ 错误
const doubled = springs.x + 10;

// ✅ 正确
transform: springs.x.to((x) => `translateX(${x * 2}px)`);

11.6 组件卸载时清理 ​

useSpring 等 hook 在组件卸载时会自动清理,不用手动取消。但如果你在 useEffect 里手动 api.start() 了一个无限循环动画,要注意卸载时停止:

tsx
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。