手动修改URL时Framer Motion退出动画失效的排查与解决
解决React路由中AnimatePresence手动输入URL时退出动画失效的问题
问题本质
手动输入URL或刷新页面时,浏览器会完全重新加载整个应用——之前的路由组件已经被彻底销毁,AnimatePresence根本没有机会触发退出动画(毕竟旧组件都不存在了)。而用navigate()是单页应用内的客户端路由跳转,组件是在React生命周期内正常卸载,所以AnimatePresence能捕获到卸载事件并执行退出动画。
解决方案
方案1:通过本地存储记录历史路由,模拟退出动画
核心思路是页面初始化时,先渲染上一次访问的路由组件并执行退出动画,完成后再切换到当前路由组件。
import { useState, useEffect } from 'react'; import { useLocation, Routes, Route } from 'react-router-dom'; import { AnimatePresence, motion } from 'framer-motion'; import Login from './Login'; import Register from './Register'; import NotFound from './NotFound'; // 维护路由与组件的映射,避免重复条件判断 const ROUTE_COMPONENTS = { '/login': <Login />, '/register': <Register />, '*': <NotFound /> }; function App() { const location = useLocation(); const [showPrevRoute, setShowPrevRoute] = useState(true); const prevRoute = localStorage.getItem('prevRoute'); // 初始化时判断是否有历史路由,没有则直接显示当前路由 useEffect(() => { if (!prevRoute) setShowPrevRoute(false); }, [prevRoute]); // 每次路由跳转时,记录当前路径到本地存储 useEffect(() => { localStorage.setItem('prevRoute', location.pathname); }, [location.pathname]); // 历史路由组件退出动画完成后,切换到当前路由 const handleExitComplete = () => { setShowPrevRoute(false); }; return ( <AnimatePresence mode="wait"> {/* 渲染历史路由组件并执行退出动画 */} {showPrevRoute && prevRoute && ( <motion.div key={prevRoute} initial={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -100 }} transition={{ duration: 0.3 }} onExitComplete={handleExitComplete} > {ROUTE_COMPONENTS[prevRoute] || ROUTE_COMPONENTS['*']} </motion.div> )} {/* 渲染当前路由组件并执行进入动画 */} {!showPrevRoute && ( <motion.div key={location.pathname} initial={{ opacity: 0, x: 100 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.3 }} > <Routes> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="*" element={<NotFound />} /> </Routes> </motion.div> )} </AnimatePresence> ); } export default App;
方案2:添加初始化过渡层(简化版)
如果觉得方案1太繁琐,可以在应用初始化时显示一个全屏过渡层,覆盖页面,等当前路由组件渲染完成后再隐藏,模拟衔接效果,让手动输入URL的跳转体验不突兀。
import { useState, useEffect } from 'react'; import { Routes, Route } from 'react-router-dom'; import { AnimatePresence, motion } from 'framer-motion'; import Login from './Login'; import Register from './Register'; import NotFound from './NotFound'; function App() { const [isInitialLoad, setIsInitialLoad] = useState(true); useEffect(() => { // 延迟隐藏过渡层,与动画时长匹配 const timer = setTimeout(() => { setIsInitialLoad(false); }, 300); return () => clearTimeout(timer); }, []); return ( <> {/* 初始化过渡层 */} {isInitialLoad && ( <motion.div className="fixed inset-0 bg-white z-50 flex items-center justify-center" initial={{ opacity: 1 }} animate={{ opacity: 0 }} transition={{ duration: 0.3 }} onAnimationComplete={() => setIsInitialLoad(false)} > {/* 可以添加加载图标或文字 */} <div>加载中...</div> </motion.div> )} {/* 原有路由动画逻辑 */} <AnimatePresence mode="wait"> <Routes location={location} key={location.pathname}> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="*" element={<NotFound />} /> </Routes> </AnimatePresence> </> ); } export default App;
注意事项
- 方案1中,可在用户关闭浏览器时清除本地存储的历史路由(比如监听
beforeunload事件),避免下次打开应用时显示旧路由。 - 两种方案的动画时长要保持一致,保证体验连贯。
- 方案1的路由映射对象需要同步更新,避免新增路由后无法匹配。
内容的提问来源于stack exchange,提问作者ali safaeyan
相关产品推荐
相关产品推荐

