React Router 与 Framer Motion 集成实现路由切换动画

514 阅读2分钟

React Router 与 Framer Motion 集成实现路由切换动画

背景

在实现路由切换动画时,我们希望先渲染离开路由的动效,再渲染进入路由的动效。为此,我们选择了 framer-motion 库作为动画解决方案。然而,在实际使用过程中,我们遇到了一些挑战,特别是在渲染离开路由组件的动效时。

问题描述

  1. 文档与实际使用的差异:

    • Framer Motion 官方文档中关于与 React Router v6 集成的示例使用了 useRoutes 钩子。
    • 然而,我们的项目使用了 createBrowserRouter,因为它支持 route.lazy(动态加载路由组件)。
  2. 动效渲染异常:

    • 使用某些写法时,路由切换会导致错误的动效顺序(新路由的离开和进入,而不是旧路由的离开和新路由的进入)。

代码对比与分析

错误写法

const location = useLocation()
<AnimatePresence mode='wait'>
    {/* 如果没显式的 key 的话,连动效都不会有 */}
    <Outlet key={location.pathname}/>
</AnimatePresence>

这种写法会导致在路由 A 切换到路由 B 时,执行 B 的离开和 B 的进入动效。

正确写法

const element = useOutlet()
const location = useLocation()
<AnimatePresence mode='wait'>
    {React.cloneElement(element, {key: location.pathname})}
</AnimatePresence>

这种写法能正确执行 A 的离开和 B 的进入动效。

原因分析

尽管 <Outlet/> 组件的源码显示它直接调用了 useOutlet,但两者在使用时仍存在关键差异:

  1. useOutlet() 返回一个固定引用,直到下次渲染才更新。
  2. <Outlet/> 每次渲染都提供新的子元素,可能导致 AnimatePresence 无法正确识别需要执行退出动画的元素。
  3. 执行上下文不同:useOutlet() 在父组件中执行,而 <Outlet/> 在子组件中执行 useOutlet()

结论

  1. AnimatePresence 的实现对动画效果有重要影响。它通过比较新旧子元素来决定哪些元素需要执行退出动画。
  2. 提供子元素给 AnimatePresence 的方式(使用 useOutlet() 还是 <Outlet/>)会影响其行为。
  3. 正确设置 key 对于 AnimatePresence 识别子元素变化至关重要。

解决方案

使用 useOutlet() 并手动控制子元素的更新。

参考实现

为了更好地理解和实现这个解决方案,您可以参考这个 CodeSandbox 示例,其中展示了一个可行的实现方式。