3、React Transition Group入门:安装与基本概念、CSSTransition组件详解、Transition组件详解、实战:列表动画

3.1 为什么需要React Transition Group?

说实话,我刚用React那会儿,做动画全靠CSS硬写。组件挂载、卸载的过渡效果?那叫一个头疼。后来发现了一个宝藏库——React Transition Group。

它不是什么动画引擎,不帮你算贝塞尔曲线,也不管你用什么CSS框架。它的核心就一件事:管理组件在进入、退出时的状态。说白了,就是告诉你「现在该加enter类了,现在该加entered类了」,剩下的动画效果你自己用CSS搞定。

我在项目中遇到过好几次这样的场景:弹窗关闭时直接消失,用户体验极差。用了这个库之后,加几行CSS就能实现淡出效果。嗯,真香。

3.2 安装与基本概念

安装很简单,一行命令搞定:

npm install react-transition-group

或者用yarn:

yarn add react-transition-group

这个库主要提供三个核心组件:

  • Transition:最底层的组件,直接操作状态机
  • CSSTransition:基于Transition封装,配合CSS类名使用
  • TransitionGroup:管理一组列表项的进出动画

你想想看,这三个组件其实是一个递进关系。Transition是基础,CSSTransition是常用方案,TransitionGroup解决批量问题。我个人习惯,90%的场景直接用CSSTransition就够了。

核心概念:动画状态机

每个组件在进入或退出时,会经历四个状态:enteringenteredexitingexited。你只需要在不同状态下定义不同的样式,过渡效果自然就有了。

3.3 CSSTransition组件详解

CSSTransition是我用得最多的组件。它接收一个in属性来控制显示隐藏,然后自动帮你切换CSS类名。

来看一个最简单的淡入淡出效果:

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

function FadeBox({ show }) {
  return (
    <CSSTransition
      in={show}
      timeout={300}
      classNames="fade"
      unmountOnExit
    >
      <div className="box">Hello World</div>
    </CSSTransition>
  );
}

对应的CSS这样写:

.fade-enter {
  opacity: 0;
}
.fade-enter-active {
  opacity: 1;
  transition: opacity 300ms;
}
.fade-exit {
  opacity: 1;
}
.fade-exit-active {
  opacity: 0;
  transition: opacity 300ms;
}

为什么会这样?因为CSSTransition在组件进入时,会依次添加fade-enterfade-enter-active类。退出时则添加fade-exitfade-exit-active。你只需要在这些类里定义样式,动画就自动跑起来了。

小技巧:classNames属性可以传对象,自定义每个状态的类名。比如classNames={{ enter: 'my-enter', enterActive: 'my-enter-active' }}。我在项目中经常这么干,避免和全局样式冲突。

几个关键属性你得记住:

属性 类型 说明
in boolean 控制显示/隐藏
timeout number | object 动画持续时间,可分别设置enter/exit
classNames string | object CSS类名前缀或映射
unmountOnExit boolean 退出后是否卸载DOM
mountOnEnter boolean 首次进入时才挂载DOM

注意:timeout的值必须和CSS中的transition时长一致。我曾经因为timeout设了500ms,CSS里写了300ms,结果动画还没结束组件就被卸载了,画面直接卡住。嗯,这种bug找起来挺费劲的。

3.4 Transition组件详解

Transition是更底层的组件。它不关心CSS,只给你四个状态回调:onEnteronEnteringonEnteredonExitonExitingonExited

什么时候用Transition?当你需要更精细的控制时。比如你想在动画开始时触发一个数据请求,或者记录日志。

import { Transition } from 'react-transition-group';

const duration = 300;

const defaultStyle = {
  transition: `opacity ${duration}ms ease-in-out`,
  opacity: 0,
};

const transitionStyles = {
  entering: { opacity: 1 },
  entered:  { opacity: 1 },
  exiting:  { opacity: 0 },
  exited:   { opacity: 0 },
};

function FadeWithTransition({ in: inProp }) {
  return (
    <Transition in={inProp} timeout={duration}>
      {(state) => (
        <div style={{
          ...defaultStyle,
          ...transitionStyles[state]
        }}>
          我是用Transition控制的
        </div>
      )}
    </Transition>
  );
}

你看,Transition用的是render props模式。它把当前状态作为参数传给你,你根据状态返回不同的样式或内容。这种方式更灵活,但代码量也更大。

我个人建议:能用CSSTransition就别用Transition。除非你需要做非CSS的动画(比如canvas动画),或者需要在动画过程中执行JS逻辑。

3.5 实战:列表动画

好了,理论说完了,咱们来点实战。列表动画是前端最常见的场景之一——新增、删除列表项时,希望有平滑的过渡效果。

这里要用到TransitionGroup组件。它包裹整个列表,配合CSSTransition实现每个项的进出动画。

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

function TodoList() {
  const [items, setItems] = useState([
    { id: 1, text: '学习React Transition Group' },
    { id: 2, text: '写一个列表动画' },
  ]);

  const addItem = () => {
    const id = Date.now();
    setItems([...items, { id, text: `新任务 ${id}` }]);
  };

  const removeItem = (id) => {
    setItems(items.filter(item => item.id !== id));
  };

  return (
    <div>
      <button onClick={addItem}>添加任务</button>
      <TransitionGroup>
        {items.map(item => (
          <CSSTransition
            key={item.id}
            timeout={500}
            classNames="item"
          >
            <div className="todo-item">
              <span>{item.text}</span>
              <button onClick={() => removeItem(item.id)}>删除</button>
            </div>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
}

CSS部分:

.item-enter {
  opacity: 0;
  transform: translateX(-50px);
}
.item-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms ease;
}
.item-exit {
  opacity: 1;
  transform: translateX(0);
}
.item-exit-active {
  opacity: 0;
  transform: translateX(50px);
  transition: all 500ms ease;
}

关键点:每个CSSTransition必须有一个唯一的key。TransitionGroup通过key来追踪哪些项是新增的、哪些是删除的。没有key,动画就乱套了。

我曾经在项目里犯过一个低级错误:给所有列表项用了同一个key。结果删除时动画全乱套,新增的项直接闪现出来。排查了半天才发现是key的问题。嗯,这种坑踩过一次就记住了。

3.6 避坑指南

最后分享几个我踩过的坑:

  • timeout不匹配:CSS的transition时长和timeout属性必须一致,否则动画会中断或卡顿
  • 忘记加unmountOnExit:如果不加,组件退出后仍然占据DOM空间,可能影响布局
  • key重复:列表动画中key必须唯一且稳定,不要用index作为key
  • 嵌套动画:如果列表项内部还有子动画,需要单独处理,TransitionGroup不会自动递归

好了,这一章的内容就到这里。下一章我们会深入探讨SwitchTransition和更高级的动画编排技巧。记得动手敲一遍代码,光看是学不会动画的。