React添加路由切换过渡动画

使用场景

在使用React router dom进行路由切换的时候,视图会直接切换,导致观感过于生硬,由此可以添加过渡动画,使过渡更加平滑。以下是两种可能的实现方案以供参考。

1. 组件懒加载+Suspense

React组件在被挂载可视前会执行一系列生命周期函数,如果待加载的组件被Suspense组件包裹,那组件在加载完毕前会被Suspense中设置的组件替代显示。

jsx 复制代码
<Suspense fallback={<Loading/>}>
  <MyComponent />
</Suspense>

这段代码实现的效果就是MyComponent组件在加载的时候会显示Loading的内容。

因此可以使用懒加载路由组件并用Suspense包裹所有路由组件的方法实现一个简单的过渡动画:

JSX 复制代码
// 路由配置文件
const Home=lazy(()=>import("../pages/Home/index.tsx"));

const routes=[
    {
        path:"/",
        element:<Home />
    }
]

// 路由挂载的组件
export default function Main() {
  const elements=useRoutes(routes);
  return (
    <main>
      <Suspense fallback={<Loading/>}>
        {elements}
      </Suspense>
    </main>
  )
}

使用ECharts实现的一个简单Loading做测试

JSX 复制代码
export default function Loading() {
  const loadingRef=useRef(null);

  useEffect(()=>{
    let loading=echarts.init(loadingRef.current);
    const option:EChartsOption = {
      graphic: {
        elements: [
          {
            type: 'group',
            left: 'center',
            top: 'center',
            children: new Array(7).fill(0).map((val, i) => ({
              type: 'rect',
              x: i * 20,
              shape: {
                x: 0,
                y: -40,
                width: 10,
                height: 80
              },
              style: {
                fill: '#ffffff'
              },
              keyframeAnimation: {
                duration: 1000,
                delay: i * 200,
                loop: true,
                keyframes: [
                  {
                    percent: 0.5,
                    scaleY: 0.3,
                    easing: 'cubicIn'
                  },
                  {
                    percent: 1,
                    scaleY: 1,
                    easing: 'cubicOut'
                  }
                ]
              }
            }))
          }
        ]
      }
    };
    loading.setOption(option);
  },[])
  return (
    <div ref={loadingRef} className={'loading-main'} />
  );
}

现在在路由页面切换时,这个Loading组件会显示,当组件加载完成时,Loading组件也会被正常卸载。

但美中不足的一点就是Loading这个组件本身也会没有过渡地突然出现和消失。我之前想过使用React的生命周期函数来动态控制CSS(添加复合类的形式),类似:

SCSS 复制代码
.loading{
    // 其他样式...
    transition: 0.3s;

    &.fade-in{
        opacity: 1;
        // 其他样式...
    }

    &.fade-out{
        opacity: 0;
        // 其他样式... 
    }
}

然后在componentDidMount时添加fade-in,在componentWillUnMount的时候添加fade-out实现Loading组件本身的过渡效果,后面测试时发现这种方法并不可行,经过搜索和自己测试,最终采取了下面的方式解决。

2. 使用react-transition-group库

react-transition-group(react-transition-group - npm)是管理组件的挂载和卸载,并提供生命周期钩子来应用CSS动画或JavaScript动画的库,使用其可以实现比较完美的过渡动画

安装

shell 复制代码
npm install react-transition-group --save

使用

创建两个state用于管理动画状态

TSX 复制代码
const [isLoading, setIsLoading] = useState(false);
const [showLoading, setShowLoading] = useState(false);

再监测路由跳转,当路由变化时设置显示Loading组件并开始动画

TSX 复制代码
useEffect(() => {
    setIsLoading(true);
    setShowLoading(true);
    loadingStartTime.current = Date.now();
}, [location.key]);

在所有路由组件的最外层使用CSSTransition包裹

TSX 复制代码
<main>
  <TransitionGroup>
    {showLoading && (
      <CSSTransition
        in={isLoading}
        timeout={500}
        classNames="loading-transition"
        unmountOnExit
      >
        <Loading />
      </CSSTransition>
    )}
    
    <CSSTransition
      key={location.key}
      timeout={500}
      classNames="page-transition"
    >
      <Suspense
        fallback={
          <span style={{ display: 'none' }} />
        }
      >
        {elements}
      </Suspense>
    </CSSTransition>
  </TransitionGroup>
</main>

并按-enter, -exit的CSS选择器名抽后缀设置CSS过渡效果-active则为状态激活后的动画效果

css 复制代码
.loading-transition-enter {
    opacity: 0;
}
  
.loading-transition-enter-active {
    opacity: 1;
    transition: opacity 150ms ease-in-out;
}

.loading-transition-exit {
    opacity: 1;
}
  
.loading-transition-exit-active {
    opacity: 0

    transition: opacity 150ms ease-in-out;
}
  
.page-transition-enter {
    opacity: 0;
}
  
.page-transition-enter-active {
    opacity: 1;
    transition: opacity 150ms ease-in-out;
}
  
.page-transition-exit {
    opacity: 1;
}
  
.page-transition-exit-active {
    opacity: 0;
    transition: opacity 150ms ease-in-out;
}

现在已经能实现Loading组件的平滑进入效果,但是有一个新的问题就是状态不会退出,一直卡在Loading的动画中,我们只设置了isLoading和showLoading为true并没有在组件加载完成之后设置回false,因此要想办法在组件加载完成时还原状态。使用Redux或给每个路由组件传递一个回调控制父组件的isLoading和showLoading状态有点过于复杂,且不符合开闭原则。

所以我们可以创建一个组件用于跟踪路由组件的加载状态,给这个跟踪组件传递回调用于改变父组件加载相关state为false。

TSX 复制代码
type Props={
  onFinish:()=>void
}

function LoadingTracker({ onFinish }:Props) {
  const [isMounted, setIsMounted] = useState(false);
  
  useEffect(() => {
    if (!isMounted) {
      setIsMounted(true);
      return;
    }
  
    onFinish();
  }, [isMounted, onFinish]);
  
  return null;
}

把这个组件放到Suspense标签内

JSX 复制代码
<Suspense
    fallback={
      <span style={{ display: 'none' }} />
    }
>
    <LoadingTracker onFinish={handleLoadingFinish} />
    {elements}
</Suspense>

添加handleLoadingFinish函数

javascript 复制代码
const handleLoadingFinish = () => {
// 最短加载时间,防止加载时间过短导致闪屏
const minimumLoadingTime = 700;
const elapsed = Date.now() - loadingStartTime.current;
const remaining = Math.max(minimumLoadingTime - elapsed, 0);

setTimeout(() => {
      setIsLoading(false);
      setShowLoading(false);
    }, remaining);
};

这段代码执行的逻辑是

  1. LoadingTracker组件被创建,同时初始化isMounted为false,然后触发useEffect设置isMounted为true
  2. Suspense加载新的路由组件(element)
  3. 路由组件加载完成后,Suspense重新渲染更新子组件(LoadingTracker和路由组件)
  4. LoadingTracker触发useEffect,执行onFinish回调,执行父组件的handleLoadingFinish
  5. 加载完毕

至此就完全实现了路由切换的加载过渡,欢迎在评论区进行指正和讨论。

相关推荐
小大宇25 分钟前
Vue知识点6-computed计算属性
前端·javascript·vue.js
程序员海军1 小时前
尤雨溪 : Vue 4 已经在规划中了!
前端·javascript·vue.js
2601_949970691 小时前
HumanizeDuration 跨端三方库仓颉适配全流程
前端·javascript·flutter
风骏时光牛马3 小时前
后端服务接口开发与性能优化
前端
kyriewen3 小时前
我让 4 个 AI 互相 review 代码,最温柔的那个下手最狠
前端·程序员·ai编程
计算机魔术师3 小时前
493家公司白砸AI钱?Apollo这组数据戳破了幻觉
前端
IT_陈寒3 小时前
Java静态变量初始化坑得我头皮发麻
前端·人工智能·后端
柳杉4 小时前
用 Codex 完成智慧高速 3D 可视化大屏,从 18 张行业设计稿到可交互的运行监测中心
前端·ai编程·数据可视化
review445434 小时前
拿到 codex 第一步
前端·agent·vibecoding
鶴哥只手遮天4 小时前
jQuery 4.0 源码架构与工程实践-第 01 章 · 全景与工程结构:一个 20 岁库的自省
前端·javascript·jquery