在 Vue3 中,<Transition> 能优雅地处理 DOM 的挂载/卸载,例如「元素销毁前先淡出,元素挂载后再淡入」。在 React 生态里,如何获得类似体验?常见动画库有 Framer Motionreact-springGSAPshadcn/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/reactuseGSAP/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 Motionreact-spring
  • 项目已在用 shadcn/uiRadix Presence + CSS;复杂时再叠加 Framer Motion/GSAP
  • 需要时间轴/序列/滚动驱动等复杂编排GSAP(记得自行处理卸载时机)。

总结

React 没有内置的过渡机制,但社区方案成熟:

  • Framer Motion / react-spring:最贴近 Vue 语义的「挂载/卸载即过渡」。
  • GSAP:强力编排工具,适用于复杂动效与时间线。
  • shadcn/ui:配合 Radix Presence 把卸载延后到动画结束,动画层自行选择。

A Student on the way to full stack of Web3.