4、React Transition Group进阶:SwitchTransition组件、TransitionGroup组件、自定义动画类名、实战:路由切换动画

好,咱们继续往下走。上一章我们把 CSSTransition 玩得挺透了,但实际项目中,动画场景远比「一个元素进进出出」要复杂。比如:两个组件切换时,旧的还没退场,新的就挤上来了,那画面简直没法看。

这一章,我就带你搞定这些「进阶玩法」。说白了,就是让你能优雅地处理多元素、多状态的动画编排。

4.1 SwitchTransition:让两个组件「有秩序」地切换

先说说 SwitchTransition。这玩意儿是干嘛的?

你想想看,一个 Tab 切换,或者一个登录/注册表单切换。旧的组件要退场,新的要进场。如果不用 SwitchTransition,默认行为是:新的直接出现,旧的瞬间消失——生硬得很。

SwitchTransition 的核心思想是:让退场动画和进场动画「串行」执行。先让旧的走完,再让新的进来。这样视觉上就流畅了。

关键点:SwitchTransition 只管理两个子元素的切换。它要求你给每个子元素一个唯一的 key,通过 key 的变化来触发动画。

来看个例子:

import { SwitchTransition, CSSTransition } from 'react-transition-group';

function Tabs({ activeTab }) {
  return (
    <SwitchTransition mode="out-in">
      <CSSTransition
        key={activeTab}
        timeout={300}
        classNames="tab"
      >
        <div className="tab-content">
          {activeTab === 'login' ? <LoginForm /> : <RegisterForm />}
        </div>
      </CSSTransition>
    </SwitchTransition>
  );
}

这里有个 mode 属性,我重点说一下:

mode 值 行为 适用场景
out-in 旧元素先退场,退场完成后新元素再进场 页面切换、Tab 切换(推荐)
in-out 新元素先进场,进场完成后旧元素再退场 通知提示、徽章数字更新

我的习惯:大部分场景用 out-in。in-out 容易造成两个元素同时存在,布局会「跳」一下。除非你明确需要新元素先出现占位,否则别轻易用 in-out。

4.2 TransitionGroup:管理「一群」元素的动画

SwitchTransition 管的是两个元素的切换。那如果是一个列表呢?比如待办事项,新增一项、删除一项、排序——每个项都要有自己的进场和退场动画。

这时候就该 TransitionGroup 上场了。

TransitionGroup 是一个容器,它会监听它内部子元素的变化(增、删、排序),然后自动为每个子元素触发对应的动画生命周期。

我举个例子,一个待办列表:

import { TransitionGroup, CSSTransition } from 'react-transition-group';

function TodoList({ todos }) {
  return (
    <TransitionGroup component="ul" className="todo-list">
      {todos.map(todo => (
        <CSSTransition
          key={todo.id}
          timeout={500}
          classNames="todo"
        >
          <li>
            {todo.text}
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        </CSSTransition>
      ))}
    </TransitionGroup>
  );
}

注意看,TransitionGroup 本身不负责动画逻辑,它只负责「感知变化」。真正的动画效果,还是由它内部的 CSSTransition 来驱动。

我曾经踩过的坑:TransitionGroup 的 component 属性默认是 'div'。如果你传了 component="ul",但子元素是 li,那没问题。但如果你传了 component={null},它就不会包裹任何 DOM 节点,这时候你得自己处理布局。我建议新手先别碰 component={null},老老实实用默认的 div 或者指定一个语义化标签。

4.3 自定义动画类名:摆脱「enter/exit」的束缚

CSSTransition 默认的 classNames 是固定的:enter、enter-active、enter-done、exit、exit-active、exit-done。但有时候,我们需要更灵活的命名。

比如,你用了某个 CSS 动画库,它的类名是 fadeIn、fadeOut。或者你团队有自己的一套命名规范。

这时候,classNames 属性可以传一个对象:

<CSSTransition
  timeout={300}
  classNames={{
    enter: 'fadeIn',
    enterActive: 'fadeIn-active',
    exit: 'fadeOut',
    exitActive: 'fadeOut-active',
  }}
>
  <div>内容</div>
</CSSTransition>

这样,动画类名就完全由你控制了。说白了,就是给 React Transition Group 和你的 CSS 框架之间搭了一座桥。

我个人习惯:如果项目用了 Tailwind CSS,我会把自定义类名和 Tailwind 的 utility class 结合起来。比如 enterActive 写成 'opacity-100 translate-y-0 transition-all duration-300'。这样连 CSS 文件都不用写了。

4.4 实战:路由切换动画

好了,理论讲完了。咱们来点真格的——给 React Router 加上页面切换动画。

这是很多面试官喜欢问的,也是实际项目中非常常见的需求。说白了,就是让用户从一个页面跳到另一个页面时,视觉上有个过渡,而不是「啪」一下变了。

核心思路:用 SwitchTransition 包裹路由内容,用路由的 location.key 作为 key 值。

来看完整实现:

import { SwitchTransition, CSSTransition } from 'react-transition-group';
import { Routes, Route, useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();

  return (
    <SwitchTransition mode="out-in">
      <CSSTransition
        key={location.key}
        timeout={300}
        classNames="page"
      >
        <div className="page-wrapper">
          <Routes location={location}>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<Contact />} />
          </Routes>
        </div>
      </CSSTransition>
    </SwitchTransition>
  );
}

对应的 CSS:

.page-enter {
  opacity: 0;
  transform: translateX(20px);
}
.page-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 300ms ease-out;
}
.page-exit {
  opacity: 1;
  transform: translateX(0);
}
.page-exit-active {
  opacity: 0;
  transform: translateX(-20px);
  transition: all 300ms ease-in;
}

注意:Routes 组件要显式传入 location={location},否则它不会感知到路由变化。这个细节我当初调试了半小时才找到原因。

为什么用 location.key 而不是 location.pathname?因为同一个路径下,如果只是 query 参数变了(比如 ?page=1 变成 ?page=2),pathname 没变,动画不会触发。而 key 每次路由变化都会生成新值,确保动画一定执行。

嗯,这里还要提一嘴:如果你用了 React Router v6,记得把 <Routes> 放在 <CSSTransition> 内部,并且用 location={location} 覆盖它的默认 location。否则动画会失效。

我曾经遇到过:在嵌套路由场景下,子路由变化时父级页面也跟着重新渲染了。解决方案是:只在最外层的路由容器做动画,子路由内部不要再用 SwitchTransition 包裹。否则动画会「套娃」,性能直接崩掉。

最后总结一下这一章的核心:

  • SwitchTransition:两个元素切换,用 mode 控制先后顺序
  • TransitionGroup:列表元素的增删排序动画
  • 自定义类名:用对象形式覆盖默认的 enter/exit 类名
  • 路由动画:SwitchTransition + location.key 实现页面切换过渡

下一章,我们会进入更高级的动画编排——用 Framer Motion 实现复杂交互动画。那玩意儿比 React Transition Group 更「声明式」,写起来也更爽。咱们到时候见。