《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>
  );
}

⚠️ 注意事项:

  • 使用 onMouseEnteronMouseLeave 控制状态
  • 可结合 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.memouseCallback

🐛 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。”


🎉 🎉

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐