React Router 与 Framer Motion 集成实现路由切换动画
背景
在实现路由切换动画时,我们希望先渲染离开路由的动效,再渲染进入路由的动效。为此,我们选择了 framer-motion 库作为动画解决方案。然而,在实际使用过程中,我们遇到了一些挑战,特别是在渲染离开路由组件的动效时。
问题描述
-
文档与实际使用的差异:
- Framer Motion 官方文档中关于与 React Router v6 集成的示例使用了
useRoutes钩子。 - 然而,我们的项目使用了
createBrowserRouter,因为它支持route.lazy(动态加载路由组件)。
- Framer Motion 官方文档中关于与 React Router v6 集成的示例使用了
-
动效渲染异常:
- 使用某些写法时,路由切换会导致错误的动效顺序(新路由的离开和进入,而不是旧路由的离开和新路由的进入)。
代码对比与分析
错误写法
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,但两者在使用时仍存在关键差异:
useOutlet()返回一个固定引用,直到下次渲染才更新。<Outlet/>每次渲染都提供新的子元素,可能导致AnimatePresence无法正确识别需要执行退出动画的元素。- 执行上下文不同:
useOutlet()在父组件中执行,而<Outlet/>在子组件中执行useOutlet()。
结论
AnimatePresence的实现对动画效果有重要影响。它通过比较新旧子元素来决定哪些元素需要执行退出动画。- 提供子元素给
AnimatePresence的方式(使用useOutlet()还是<Outlet/>)会影响其行为。 - 正确设置 key 对于
AnimatePresence识别子元素变化至关重要。
解决方案
使用 useOutlet() 并手动控制子元素的更新。
参考实现
为了更好地理解和实现这个解决方案,您可以参考这个 CodeSandbox 示例,其中展示了一个可行的实现方式。