React Router useNavigate上下文报错,寻求无刷新跳转方案
问题描述
在middleWare.js中使用useNavigate重定向到/时,触发错误:useNavigate() may be used only in the context of a window.location.href虽然能实现跳转,但会导致页面全量刷新,网站出现卡顿。
问题根源
useAuth钩子在App组件中被调用,而App组件位于<Router>组件的外部,导致useNavigate无法获取到路由上下文,因此报错。
解决方案
将认证逻辑的调用移到<Router>包裹的内部组件中,确保useNavigate能访问到路由上下文,同时用navigate替代window.location.href实现无刷新跳转。
修改后的代码
App.js
import './App.css'; import { BrowserRouter as Router, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import Login from './components/Login'; import Callback from './components/CallBack'; import Home from './components/Home'; import ResponsiveAppBar from './components/NavBar'; import Profile from './components/Profile'; import { useEffect, useState } from 'react'; import axios from 'axios'; import { refreshAccessToken } from './spotifyAuth'; import { useAuth } from './middleWare'; function App() { return ( <Router> <AppWithAppBar /> </Router> ); } const AppWithAppBar = () => { const [userData, setUserData] = useState(null); const [token, setToken] = useState(localStorage.getItem('spotifyAccessToken')); const isAuthenticated = useAuth(); const [isLoading, setIsLoading] = useState(true); const location = useLocation(); const shouldShowAppBar = location.pathname !== '/'; const navigate = useNavigate(); // 获取用户数据 const fetchUserData = async () => { try { const response = await axios.get('https://api.spotify.com/v1/me', { headers: { Authorization: `Bearer ${token}`, }, }); setUserData(response.data); } catch (error) { console.error('获取用户数据失败:', error); } }; // 注销函数 const handleLogout = () => { localStorage.removeItem('spotifyAccessToken'); localStorage.removeItem('spotifyRefreshToken'); localStorage.removeItem('spotifyTokenExpiry'); setUserData(null); navigate('/'); // 无刷新跳转 }; // 检查令牌过期 const checkTokenExpiration = async () => { const expiry = localStorage.getItem('spotifyTokenExpiry'); if (Date.now() >= expiry) { try { const refreshToken = localStorage.getItem('spotifyRefreshToken'); const newTokens = await refreshAccessToken(refreshToken); localStorage.setItem('spotifyAccessToken', newTokens.access_token); localStorage.setItem('spotifyTokenExpiry', Date.now() + newTokens.expires_in * 1000); setToken(newTokens.access_token); } catch (error) { console.error('刷新令牌失败:', error); } } }; useEffect(() => { if (isAuthenticated) { fetchUserData(); } setIsLoading(false); }, [isAuthenticated]); // 加载状态显示 if (isLoading) { return ( <div style={loadingContainerStyle}> <div style={loadingSpinnerStyle}></div> <h2 style={{ color: '#fff' }}>加载中...</h2> </div> ); } return ( <> {shouldShowAppBar && ( <div style={{ position: 'fixed', width: '100%', top: 0, zIndex: 1000 }}> <ResponsiveAppBar handleLogout={handleLogout} /> </div> )} <div style={{ paddingTop: shouldShowAppBar ? '64px' : '0px' }}> <Routes> <Route path="/" element={<Login />} /> <Route path="/callback" element={<Callback />} /> {isAuthenticated ? ( <> <Route path="/home" element={<Home userData={userData} handleLogout={handleLogout} />} /> <Route path="/profile" element={<Profile userData={userData} />} /> </> ) : ( <Route path="*" element={<Login />} /> )} </Routes> </div> </> ); }; const loadingContainerStyle = { height: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', backgroundColor: '#1e1e2f', }; const loadingSpinnerStyle = { width: '50px', height: '50px', border: '6px solid #a29bfe', borderTop: '6px solid transparent', borderRadius: '50%', animation: 'spin 1s linear infinite', }; const spinnerAnimationStyle = document.createElement('style'); spinnerAnimationStyle.innerHTML = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(spinnerAnimationStyle); export default App;
middleWare.js(无需修改)
import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { refreshAccessToken } from './spotifyAuth'; export const useAuth = () => { const [isAuthenticated, setIsAuthenticated] = useState(false); const navigate = useNavigate(); useEffect(() => { const checkAuth = async () => { const token = localStorage.getItem('spotifyAccessToken'); const expiry = localStorage.getItem('spotifyTokenExpiry'); if (!token || Date.now() >= expiry) { try { const refreshToken = localStorage.getItem('spotifyRefreshToken'); if (refreshToken) { const newTokens = await refreshAccessToken(refreshToken); localStorage.setItem('spotifyAccessToken', newTokens.access_token); localStorage.setItem('spotifyTokenExpiry', Date.now() + newTokens.expires_in * 1000); setIsAuthenticated(true); } else { navigate('/'); } } catch (error) { navigate('/'); } } else { setIsAuthenticated(true); } }; checkAuth(); }, [navigate]); return isAuthenticated; };
修改说明
- 将
App组件内的状态、逻辑全部迁移到AppWithAppBar组件中,该组件位于<Router>内部,确保useAuth调用时useNavigate能获取到路由上下文。 - 注销函数改用
navigate('/')实现无刷新跳转,避免页面卡顿。 - 路由配置中增加未认证时的兜底跳转,确保未登录用户只能访问登录页和回调页。
内容的提问来源于stack exchange,提问作者Amit
相关产品推荐
相关产品推荐

