You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Router useNavigate上下文报错,寻求无刷新跳转方案

问题解决:useNavigate上下文错误与页面刷新卡顿

问题描述

在middleWare.js中使用useNavigate重定向到/时,触发错误:useNavigate() may be used only in the context of a component。改用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;
};

修改说明

  1. 将App组件内的状态、逻辑全部迁移到AppWithAppBar组件中,该组件位于<Router>内部,确保useAuth调用时useNavigate能获取到路由上下文。
  2. 注销函数改用navigate('/')实现无刷新跳转,避免页面卡顿。
  3. 路由配置中增加未认证时的兜底跳转,确保未登录用户只能访问登录页和回调页。

内容的提问来源于stack exchange,提问作者Amit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:42:32