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的动画中,我们只设置了isLoadingshowLoadingtrue并没有在组件加载完成之后设置回false,因此要想办法在组件加载完成时还原状态。使用Redux或给每个路由组件传递一个回调控制父组件的isLoadingshowLoading状态有点过于复杂,且不符合开闭原则。

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

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组件被创建,同时初始化isMountedfalse,然后触发useEffect设置isMountedtrue
  2. Suspense加载新的路由组件(element
  3. 路由组件加载完成后,Suspense重新渲染更新子组件(LoadingTracker和路由组件)
  4. LoadingTracker触发useEffect,执行onFinish回调,执行父组件的handleLoadingFinish
  5. 加载完毕

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

相关推荐
To_OC5 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic5 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy5 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志6 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble7 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯8 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜8 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司9 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g9 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森9 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http