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

相关推荐
u01040583617 分钟前
构建可扩展的Java Web应用架构
java·前端·架构
swimxu1 小时前
npm 淘宝镜像证书过期,错误信息 Could not retrieve https://npm.taobao.org/mirrors/node/latest
前端·npm·node.js
qq_332394201 小时前
pnpm的坑
前端·vue·pnpm
雾岛听风来1 小时前
前端开发 如何高效落地 Design Token
前端
不如吃茶去1 小时前
一文搞懂React Hooks闭包问题
前端·javascript·react.js
alwn1 小时前
新知识get,vue3是如何实现在style中使用响应式变量?
前端
来之梦1 小时前
uniapp中 uni.previewImage用法
前端·javascript·uni-app
野猪佩奇0072 小时前
uni-app使用ucharts地图,自定义Tooltip鼠标悬浮显示内容并且根据@getIndex点击事件获取点击的地区下标和地区名
前端·javascript·vue.js·uni-app·echarts·ucharts
生活、追梦者2 小时前
html+css+JavaScript 实现两个输入框的反转动画
javascript·css·html
2401_857026232 小时前
拖动未来:WebKit 完美融合拖放API的交互艺术
前端·交互·webkit