聊一聊 React Transition Group

前言

React Transition Group 是 React 官方提供的一个工具库,用于管理和实现 React 元素在不同时机(如 进入、退出 等)应用的「过渡动画」。

例举一个常见场景:我们要实现一个 Modal 弹框,在 open state 为 true 时打开弹框,弹框会自带一个 mask 底部蒙层,借助 Transition 可以轻松实现底部蒙层淡入淡出的过渡效果。

但要注意:React Transition Group 不能算作是动画库,因为它只提供触发机制(不同阶段的状态)、具体动画需要使用者自行编写。

React Transition Group 提供了一组可供在不同场景使用的过渡组件:TransitionCSSTransitionSwitchTransitionTransitionGroup

其中比较常用的是:TransitionCSSTransition

一、Transition

Transition 作为基础组件的存在,用于实现一个 UI 组件从 进入状态 到 退出状态 的转换。它不会改变呈现组件的行为,只跟踪组件的"进入"和"退出"状态,赋予这些状态意义和效果取决于你

我们来看一个基础使用示例:元素进入和离开时,使用透明度实现淡入淡出过渡效果:

jsx 复制代码
import { useState } from "react";
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 App() {
  const [inProp, setInProp] = useState(false);

  return (
    <div>
      <Transition in={inProp} timeout={duration}>
        {(state) => (
          <div
            style={{
              ...defaultStyle,
              ...transitionStyles[state],
            }}
          >
            I'm a fade Transition!
          </div>
        )}
      </Transition>
      <button onClick={() => setInProp(!inProp)}>Click to Enter</button>
    </div>
  );
}

export default App;

Transition 作为一个 React 包裹组件使用,它包含三个属性信息:

  • in:boolean 值,表示是否进入,基于此变量来处理 进入/退出;
  • timeout:number 值,控制动画过渡所需的时长;
  • children:一个函数,返回要应用过渡机制的内容元素;

children 函数中,通过参数 state 可以获取每个过渡阶段的状态,并基于此状态来为内容元素赋予效果。state 主要处于四个状态:'entering'、'entered'、'exiting'、'exited'

二、CSSTransition

CSSTransitionTransition 基础上,增加了将 进入/退出 状态以 className 方式设置在内容元素上,此外还扩展了「入场 appear」状态。

一个简单的 透明度过渡 淡入淡出 示例如下:

jsx 复制代码
import { useState, useRef } from "react";
import { CSSTransition } from "react-transition-group";
import "./App.css";

function App() {
  const [inProp, setInProp] = useState(false);
  const nodeRef = useRef(null);
  return (
    <div>
      <CSSTransition nodeRef={nodeRef} in={inProp} timeout={200} classNames="my-node" unmountOnExit>
        <div ref={nodeRef}>
          {"I'll receive my-node-* classes"}
        </div>
      </CSSTransition>
      <button type="button" onClick={() => setInProp(!inProp)}>
        Click to Enter
      </button>
    </div>
  );
}

export default App;

// css
.my-node-enter {
  opacity: 0;
}
.my-node-enter-active {
  opacity: 1;
  transition: opacity 200ms;
}
.my-node-exit {
  opacity: 1;
}
.my-node-exit-active {
  opacity: 0;
  transition: opacity 200ms;
}

Transition 基础上增加了 classNames 属性,将过渡状态以 className 方式设置到 nodeRef 节点上。

每个阶段下 className 后缀如下:

perl 复制代码
my-node-appear, my-node-appear-active, my-node-appear-done
my-node-enter, my-node-enter-active, my-node-enter-done
my-node-exit, my-node-exit-active, my-node-exit-done

三、实现一个 useTransition

我们从一个 Modal 交互功能 说起。

实现一个 Modal 弹框功能,并期望 mask 蒙层元素能够借助 CSS opacity + transition 来实现淡入淡出过渡效果,我们写出的初版代码如下:

jsx 复制代码
import { useState } from "react";

const styles = {
  root: {
    position: "fixed",
    inset: 0,
    zIndex: 1000,
  },
  mask: {
    position: "fixed",
    inset: 0,
    zIndex: -1,
    backgroundColor: "rgba(0, 0, 0, 0.5)",
    opacity: 0,
    transition: "opacity .3s",
  }
}

function App() {
  const [open, setOpen] = useState(false);

  return (
    <div>
      {open ? (
        <div className="modal" style={styles.root}>
          <div style={{ ...styles.mask, opacity: open ? 1 : 0 }} onClick={() => setOpen(false)}></div>
          <div>Modal 内容</div>
        </div>
      ) : null}
      <button onClick={() => setOpen(true)}>打开 Modal</button>
    </div>
  )
}

export default App;

现在,通过打开关闭 Modal 你会发现 mask 蒙层并没有 opacity 淡入淡出 的过渡效果。其实很好理解原因:在 挂载/销毁 DOM 时没有留出执行过渡的时间。

所以我们要做的是:挂载 DOM 后给予时间去执行 淡入 过渡动画,销毁 DOM 前要留出时间先去执行 淡出 过渡动画

这其实就是 Transition 核心逻辑,能够控制执行阶段。

下面我们通过封装一个 hook:useTransition 来实现 Transition 能力:

jsx 复制代码
import { useState, useRef, useEffect } from "react";

type TStageType = "enter" | "leave";
type TStageDurations = Record<TStageType, number>;

const useTransition = (
  open: boolean,
  durations: TStageDurations = { enter: 0, leave: 0 }
) => {
  // 在外部 open state 基础上,使用新的 show state 来控制 Transition 时机
  const [show, setShow] = useState(open);
  // Transition status
  const [stage, setStage] = useState<TStageType | "">("");
  const timerRef = useRef<TStageDurations>({
    enter: undefined,
    leave: undefined,
  });

  useEffect(() => {
    if (open) {
      // 1. 先挂载 DOM,借助计时器延迟使用 进入 动画
      setShow(true);
      timerRef.current.enter = window.setTimeout(() => {
        setStage("enter");
      }, durations.enter);
    } else {
      // 2. 先执行 退出 动画,再执行销毁 DOM
      setStage("leave");
      timerRef.current.leave = window.setTimeout(() => {
        setShow(false);
      }, durations.leave);
    }
    return () => {
      window.clearTimeout(timerRef.current.enter);
      window.clearTimeout(timerRef.current.leave);
    };
  }, [open, durations]);

  return { show, stage };
};

export default useTransition;

使用方式如下:

jsx 复制代码
function App() {
  const [open, setOpen] = useState(false);
  const { show, stage } = useTransition(open, { enter: 0, leave: 300 });

  return (
    <div>
      {show ? (
        <div className="modal" style={styles.root}>
          <div style={{ ...styles.mask, opacity: stage === "enter" ? 1 : 0, }} onClick={() => setOpen(false)}></div>
          <div>Modal 内容</div>
        </div>
      ) : null}
      <button onClick={() => setOpen(true)}>打开 Modal</button>
    </div>
  )
}

最后

感谢阅读,如有不足之处,欢迎指出。

参考: React 离开动画实现原理 | 动画 | spring | motion

相关推荐
weixin_382395238 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__8 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.9 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~10 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华11 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子13 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅13 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni14 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学15 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端