在 Vue3 中,<Transition>
能优雅地处理 DOM 的挂载/卸载,例如「元素销毁前先淡出,元素挂载后再淡入」。在 React 生态里,如何获得类似体验?常见动画库有 Framer Motion、react-spring、GSAP 和 shadcn/ui。本文对比它们在「先淡出再卸载、先挂载再淡入」这一需求下的支持情况,并提供最小可用示例与一份基于 react-spring 的通用封装组件。
结论概览
- Framer Motion ✅:开箱即用,
AnimatePresence + exit
直接实现。 - react-spring ✅:
useTransition
天然支持挂载/卸载动画。 - GSAP ✅:能力最强,但需自行控制卸载(或配合
@gsap/react
)。 - shadcn/ui ❌:本身不是动画库;需结合 Radix Presence + CSS / Framer Motion / GSAP。
Framer Motion:最接近 Vue <Transition>
的语义
import { AnimatePresence, motion } from "framer-motion";
export default function Demo({ show }: { show: boolean }) {
return (
<AnimatePresence>
{show && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }} // 先淡出,再卸载
>
内容
</motion.div>
)}
</AnimatePresence>
);
}
要点:AnimatePresence
会在 show
变为 false
时保留节点,直到 exit
动画完成后再卸载。
react-spring:useTransition
的挂载/卸载动画
import { useTransition, animated } from "@react-spring/web";
export default function Demo({ show }: { show: boolean }) {
const transitions = useTransition(show, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }, // 动画完成后自动卸载
});
return transitions((styles, item) =>
item ? <animated.div style={styles}>内容</animated.div> : null
);
}
要点:useTransition
会根据布尔值自动处理元素的进/出场与最终卸载。你也可以传入 config
使用物理参数或固定时长。
✅ 基于 react-spring 的通用 FadeTransition
组件
适合在项目中复用的淡入淡出封装:支持布尔控制、可选固定时长或弹簧物理配置、可自定义起止透明度与样式。
import React from "react";
import { useTransition, animated, SpringConfig } from "@react-spring/web";
import { cn } from "@/lib/utils";
interface FadeTransitionProps {
/** 控制组件显示/隐藏的布尔值 */
show: boolean;
/** 要渲染的子元素 */
children: React.ReactNode;
/** 动画配置 */
config?: SpringConfig | { duration?: number };
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
/** 入场时的初始透明度 */
from?: { opacity?: number };
/** 完全显示时的透明度 */
enter?: { opacity?: number };
/** 退场时的透明度 */
leave?: { opacity?: number };
}
/**
* 通用的淡入淡出过渡动画组件
*
* @example
* ```tsx
* <FadeTransition show={isLoading}>
* <div>Loading...</div>
* </FadeTransition>
* ```
*
* @example
* ```tsx
* <FadeTransition
* show={isVisible}
* config={{ duration: 300 }}
* className="absolute inset-0"
* >
* <div>Modal Content</div>
* </FadeTransition>
* ```
*/
export const FadeTransition: React.FC<FadeTransitionProps> = ({
show,
children,
config = { duration: 200 },
className,
style,
from = { opacity: 0 },
enter = { opacity: 1 },
leave = { opacity: 0 },
}) => {
const transitions = useTransition(show, {
from,
enter,
leave,
config,
});
return (
<>
{transitions((animatedStyle, item) =>
item ? (
<animated.div
style={{
...animatedStyle,
...style,
}}
className={cn(className)}
>
{children}
</animated.div>
) : null
)}
</>
);
};
export default FadeTransition;
使用建议
- 如果你想把「布尔控制 + 挂载/卸载 + 可配置动画」下沉到组件层,这个封装很合适。
- 复杂场景可再包装:比如加入
onExited
回调(退场动画结束通知父级)、unmountOnExit
/mountOnEnter
之类的语义开关;或在内部处理prefers-reduced-motion
以照顾无障碍。
GSAP:高自由度但需手动控制
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
export default function Demo({ show }: { show: boolean }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (show) {
gsap.fromTo(el, { opacity: 0 }, { opacity: 1, duration: 0.3 });
} else {
gsap.to(el, {
opacity: 0,
duration: 0.3,
onComplete: () => setUnmount(true), // 伪代码:触发父组件卸载
});
}
}, [show]);
return show ? <div ref={ref}>内容</div> : null;
}
要点:现实中常用本地 isMounted
/isVisible
两段式状态;或借助 @gsap/react
的 useGSAP/context
简化选择器与清理。
shadcn/ui + Radix Presence:延迟卸载 + 自定义动画
import { Presence } from "@radix-ui/react-presence";
import "./fade.css";
export default function Demo({ show }: { show: boolean }) {
return (
<Presence present={show}>
<div className={show ? "fadeIn" : "fadeOut"}>内容</div>
</Presence>
);
}
fade.css:
.fadeIn { animation: fadeIn .2s forwards; }
.fadeOut { animation: fadeOut .2s forwards; }
@keyframes fadeIn { from {opacity:0} to {opacity:1} }
@keyframes fadeOut { from {opacity:1} to {opacity:0} }
要点:Radix Presence
会在 present=false
时 延迟卸载,直到动画结束。动画本身可以是 CSS、Framer Motion 或 GSAP。
选型建议
- 想要 Vue
<Transition>
的开箱体验 → Framer Motion 或 react-spring。 - 项目已在用 shadcn/ui → Radix Presence + CSS;复杂时再叠加 Framer Motion/GSAP。
- 需要时间轴/序列/滚动驱动等复杂编排 → GSAP(记得自行处理卸载时机)。
总结
React 没有内置的过渡机制,但社区方案成熟:
- Framer Motion / react-spring:最贴近 Vue 语义的「挂载/卸载即过渡」。
- GSAP:强力编排工具,适用于复杂动效与时间线。
- shadcn/ui:配合 Radix Presence 把卸载延后到动画结束,动画层自行选择。
Comments NOTHING