聊一聊 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

相关推荐
To_OC6 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC6 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic6 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy6 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志7 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble8 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长8 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯9 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜9 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司10 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能