react-spring 路由过渡动画

897 阅读3分钟

分析

过渡动画一般是两个 div

div1 稳定状态 => 退出状态

div2 进入状态 => 稳定状态

路由过渡动画

import { animated, useTransition } from '@react-spring/web'
import { Outlet, useLocation } from 'react-router-dom'

export const WelcomeLayout: React.FC = () => {
  const location = useLocation() // 获取当前地址栏的信息
  // location.pathname 获取当前的路径
  // 假设路径为:
  // location.pathname === /welcome/1  旧路由
  // location.pathname === /welcome/2  新路由
  const transitions = useTransition(location.pathname, {
    //  进入状态
    form: { transform: 'translateX(100%)' }, // 从屏幕的右边进入
    // 稳定状态
    enter: { transform: 'translateX(0%)' },
    // 退出状态
    leave: { transform: 'translateX(-100%)' },
    config: { duration: 4000 }
  })
  // 当我们切换路由的时候,从旧路由到新路由, 
  // useTransition 作用就是你每次有一个路由我就给你创建一个过渡动画,
  // 旧过渡和新过渡就放在 transitions 里面, 
  // 这个 transitions 数组里面我怎么去表示它每一个动画用什么元素?
  return transitions((style, pathname) => {
    // 用以下方法表示 transitions 数组里面的每一个动画用的元素
    // transitions 会返回两个 div
    // 当为旧路由的时候返回 animated.div 表示第一个
    // 当为新路由的时候返回 animated.div 表示第二个
    // 这里为何写一个 key,因为这是一个遍历
    // 这个 style 就是不停变化的样式
    // 这里的 style 从 100% => 0% 
    // 每次变就会把这个 style 赋值到这个 div 上面,
    // 但这个 div 不能是普通的 div 
    // 所以要用 animated.div,key 表示标符, style 表示它的状态在不停的变化
    return <animated.div key={pathname} style={style}>
      // 里面就是正常的写代码
      <div style={{ textAlign: 'center' }}>
        <Outlet/>
      </div>
    </animated.div>
  })
}

以上代码案例预览: react-spring-transition.gif 可以发现当我点击下一页的时候,页面提前更新了。

因为使用了 Outlet 表示子元素,当 path 改变的时候 这个 Outlet 就会改变。

使用 Outlet 会自己变,那么这里就不能使用这种写法,那么有什么办法可以记录每一次的 Outlet 然后把它放到这个位置上。

使用哈希表来做缓存,并且注意每次不要用最新的值,就可以解决路径切换的时候新旧同时显示的问题。

import { animated, useTransition } from '@react-spring/web'
import type { ReactNode } from 'react'
import { useLocation, useOutlet } from 'react-router-dom'

const map: Record<string, ReactNode> = {}
export const WelcomeLayout: React.FC = () => {
  const location = useLocation() // 获取当前地址栏最新的信息
  // location.pathname === /welcome/1  旧路由
  // location.pathname === /welcome/2  新路由

  // 拿到对应的 outlet, 也就是拿到当前显示的子组件
  const outlet = useOutlet()
  // 每进入一个 location 我就把它放到 map 里面
  // welcome/1 我就将其对应的outlet存下来,welcome/2 我就将其对应的outlet存下来
  map[location.pathname] = outlet // 将其存到 map 里面
  const transitions = useTransition(location.pathname, {
    //  进入状态
    form: { transform: 'translateX(100%)' },
    // 稳定状态
    enter: { transform: 'translateX(0%)' },
    leave: { transform: 'translateX(-100%)' },
    config: { duration: 4000 }
  })
  return transitions((style, pathname) => {
    return <animated.div key={pathname} style={style}>
      <div style={{ textAlign: 'center' }}>
        {/* 这里显示从 map 拿到的, location.pathname 永远都是最新的,pathname 是旧的 */}
        {map[pathname]}
      </div>
    </animated.div>
  })
}

以上代码案例预览: react-spring-transition-pathname.gif

最终代码

以上代码只要你的路径不是 welcome 开头, 它就会把这个 WelcomeLayout 组件摘掉,但并不代表 JS 内存中没有 map,这个 map 和这个组件的生命周期并不相同,map 只要一初始化就永远存在于 JS 的内存中,而 WelcomeLayout 只会在路径以 welcome 开头的时候才存在,所以就会导致内存泄漏,当我们的 WelcomeLayout 不存在的时候,它的几个 outlet 会存在 map 里面,我们需要 map 和 WelcomeLayout 生命周期同步,于是把 map 放到 WelcomeLayout 里面,并用 useRef 让其数据不会刷新

import { animated, useTransition } from '@react-spring/web'
import type { ReactNode } from 'react'
import { useRef } from 'react'
import { useLocation, useOutlet } from 'react-router-dom'

export const WelcomeLayout: React.FC = () => {
  // 使用 useRef 不会刷新数据
  const map = useRef<Record<string, ReactNode>>({})  
  const location = useLocation()
  const outlet = useOutlet()
  map.current[location.pathname] = outlet
  const transitions = useTransition(location.pathname, {
    form: {
      transform: location.pathname === '/welcome/1'
        ? 'translateX(0%)'
        : 'translateX(100%)'
    },
    enter: { transform: 'translateX(0%)' },
    leave: { transform: 'translateX(-100%)' },
    config: { duration: 4000 }
  })
  return transitions((style, pathname) =>
    <animated.div key={pathname} style={style}>
      {map.current[pathname]}
    </animated.div>
  )
}