使用Framer Motion路由动画时React Context状态丢失问题求助
解决React Router v6 + Framer Motion路由动画导致Context状态丢失的问题
核心问题分析
你遇到的问题本质是路由切换时ChatLayout(Context Provider)被意外卸载重建:当给Routes组件设置key={location.pathname}时,从/chat/join跳转到/chat/room会触发Routes的完全重渲染,连带包裹在其中的ChatLayout被卸载,导致Context存储的聊天室信息丢失。
可行解决方案
调整组件层级和动画触发逻辑,既保留Context状态,又实现完整的进出动画:
1. 重构路由层级,让ChatLayout作为父路由组件
将ChatLayout设置为/chat路径的父路由,子路由对应join和room页面。这样子路由切换时,ChatLayout不会被卸载,Context状态自然保留。
2. 用location.key替代pathname触发动画
将AnimatePresence包裹在Routes外层,使用location.key作为Routes的key——这个key是React Router为每个路由跳转生成的唯一标识,既能触发AnimatePresence的动画逻辑,又不会因为子路由path变化导致父组件卸载。
完整代码示例
App组件(路由入口)
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom'; import { AnimatePresence } from 'framer-motion'; import ChatLayout from './ChatLayout'; import ChatJoin from './ChatJoin'; import ChatRoom from './ChatRoom'; function AnimatedRoutes() { const location = useLocation(); return ( <AnimatePresence exitBeforeEnter> {/* 使用location.key触发动画,而非pathname */} <Routes key={location.key}> {/* 父路由挂载ChatLayout,子路由通过Outlet渲染 */} <Route path="/chat" element={<ChatLayout />}> <Route path="join" element={<ChatJoin />} /> <Route path="room" element={<ChatRoom />} /> </Route> </Routes> </AnimatePresence> ); } function App() { return ( <Router> <AnimatedRoutes /> </Router> ); } export default App;
ChatLayout组件(带Context)
import { createContext, useContext, useState } from 'react'; import { Outlet } from 'react-router-dom'; // 创建Chat Context const ChatContext = createContext(); export const useChatContext = () => useContext(ChatContext); export default function ChatLayout() { // 聊天室状态,子组件共享 const [chatRoomInfo, setChatRoomInfo] = useState(null); return ( <ChatContext.Provider value={{ chatRoomInfo, setChatRoomInfo }}> {/* 这里可以放聊天室公共UI,比如顶部导航、房间信息栏 */} <div className="chat-layout"> <Outlet /> {/* 渲染子路由页面 */} </div> </ChatContext.Provider> ); }
封装动画页面组件
创建一个通用的动画包裹组件,统一处理页面进出动画:
import { motion } from 'framer-motion'; export default function AnimatedPage({ children }) { return ( <motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.3 }} style={{ minHeight: '100vh' }} > {children} </motion.div> ); }
ChatJoin和ChatRoom组件(使用动画和Context)
// ChatJoin.jsx import { useChatContext } from './ChatLayout'; import AnimatedPage from './AnimatedPage'; import { useNavigate } from 'react-router-dom'; export default function ChatJoin() { const { setChatRoomInfo } = useChatContext(); const navigate = useNavigate(); const handleJoin = () => { // 模拟设置聊天室信息 setChatRoomInfo({ id: 'dev-room-01', name: '前端技术交流群' }); navigate('/chat/room'); }; return ( <AnimatedPage> <div className="chat-join"> <h2>加入聊天室</h2> <button onClick={handleJoin}>进入测试房间</button> </div> </AnimatedPage> ); }
// ChatRoom.jsx import { useChatContext } from './ChatLayout'; import AnimatedPage from './AnimatedPage'; import { useNavigate } from 'react-router-dom'; export default function ChatRoom() { const { chatRoomInfo } = useChatContext(); const navigate = useNavigate(); // 未选择房间时跳转回join页面 if (!chatRoomInfo) { navigate('/chat/join'); return null; } return ( <AnimatedPage> <div className="chat-room"> <h2>聊天室:{chatRoomInfo.name}</h2> <p>房间ID:{chatRoomInfo.id}</p> <button onClick={() => navigate('/chat/join')}>退出房间</button> </div> </AnimatedPage> ); }
方案为什么有效
- Context状态保留:ChatLayout作为父路由组件,子路由切换时不会被卸载,因此Context中的状态不会丢失。
- 完整动画支持:
AnimatePresence包裹Routes,通过location.key触发动画,确保页面切换时退出动画先执行,再进入新页面的动画。 - 兼容原有架构:不需要移除ChatContext或ChatLayout,完全符合你的需求。
为什么之前的方案无效
- 移除Routes的key:AnimatePresence无法检测到路由变化,因为Routes没有重新渲染,导致退出动画无法触发。
- AnimatePresence移至ChatLayout内:此时AnimatePresence仅包裹
Outlet,子路由切换时,组件卸载的信号无法被AnimatePresence正确捕获,导致退出动画失效。
内容的提问来源于stack exchange,提问作者Sharif Velásquez Alzate
相关产品推荐
相关产品推荐

