React Router v6刷新受保护路由页面总是重定向到首页
问题描述
应用中已配置受保护路由、Firebase登录功能,以及用于存储登录状态的Redux store,预期效果是任意页面刷新后仍停留在当前页,但实际刷新时总会被重定向到LandingPage组件。
尝试过的方案存在以下问题:
- 搜到的部分旧方案使用
useHistory钩子,但React Router v6已移除该API,无法使用 - 尝试使用
useNavigate钩子时抛出错误,提示在BrowserRouter上下文外部调用该钩子
现有代码
App.js
import { useEffect } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { login, logout, selectUser } from '../features/userSlice'; import { auth, onAuthStateChanged } from '../firebase'; import ProtectedRoute from '../scrpits/ProtectedRoutes'; import { AuthProvider } from '../contexts/AuthContext'; import { MainLayout } from '../scrpits/partials/MainLayout'; import { Home } from '../scrpits/Home'; import { LandingPage } from '../scrpits/LandingPage'; import { Profile } from '../scrpits/Profile'; import { Categories } from '../scrpits/Categories'; import { AddPage } from '../scrpits/AddPage'; import { ViewPage } from '../scrpits/ViewPage'; import '../styles/App.css'; const App = () => { const user = useSelector(selectUser); const dispatch = useDispatch(); useEffect(() => { onAuthStateChanged(auth, (userAuth) => { if (userAuth) { //logged in dispatch( login({ email: userAuth.email, uid: userAuth.uid, }) ); } else { dispatch(logout()); } }); }, []); return ( <div className="App"> <BrowserRouter> <AuthProvider> <Routes> <Route exact strict path="/" element={<LandingPage />} /> <Route element={<MainLayout />}> <Route path="home" element={ <ProtectedRoute> <Home /> </ProtectedRoute> } /> <Route path="profile" element={ <ProtectedRoute> <Profile /> </ProtectedRoute> } /> <Route path="categories" element={ <ProtectedRoute> <Categories /> </ProtectedRoute> } /> <Route path="addpage" element={ <ProtectedRoute> <AddPage /> </ProtectedRoute> } /> <Route path="viewpage" element={ <ProtectedRoute> <ViewPage /> </ProtectedRoute> } ></Route> </Route> </Routes> </AuthProvider> </BrowserRouter> </div> ); }; export default App;
原代码缺少useEffect的引入,运行会直接报错,上述代码已补全该引入
ProtectedRoute 组件
import React from 'react'; import { Navigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; const ProtectedRoute = ({ children }) => { const { currentUser } = useAuth(); console.log('Check user in Private: ', currentUser); if (!currentUser) { return <Navigate to="/" replace />; } return children; }; export default ProtectedRoute;
AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react'; import { createUserWithEmailAndPassword, signInWithEmailAndPassword, onAuthStateChanged, signOut, GoogleAuthProvider, signInWithPopup, } from 'firebase/auth'; import { auth } from '../firebase'; import { useDispatch } from 'react-redux'; import { login as loginUser } from '../features/userSlice'; const AuthContext = createContext(); export function useAuth() { return useContext(AuthContext); } export function AuthProvider({ children }) { const [currentUser, setCurrentUser] = useState(); const dispatch = useDispatch(); function signup(email, password) { return createUserWithEmailAndPassword(auth, email, password); } function login(email, password) { return signInWithEmailAndPassword(auth, email, password).then( (userAuth) => { dispatch( loginUser({ email: userAuth.email, uid: userAuth.uid, }) ); } ); } function googleSignIn() { const googleAuthProvider = new GoogleAuthProvider(); return signInWithPopup(auth, googleAuthProvider); } function logout() { return signOut(auth); } useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setCurrentUser(currentUser); }); return unsubscribe; }, []); const value = { currentUser, signup, login, logout, googleSignIn, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }
问题原因
刷新被重定向的核心原因是Firebase鉴权状态初始化是异步过程:
- 页面刚加载时,
AuthContext里的currentUser默认值是undefined,此时onAuthStateChanged的回调还没触发,还没拿到本地缓存的用户登录态 ProtectedRoute在鉴权状态未确认时就做了判断,发现currentUser为空,直接执行重定向跳转到首页- 等鉴权回调执行完成拿到用户信息时,已经完成跳转,自然无法停留在原页面
另外useNavigate报错的原因是:如果把调用useNavigate的组件写在BrowserRouter外层,就会提示不在路由上下文内,只需要保证所有调用路由相关钩子的组件都在BrowserRouter的子节点范围内即可。
修复方案
第一步:给AuthContext增加加载状态
修改AuthContext.js,新增loading状态标识鉴权初始化是否完成,在onAuthStateChanged第一次触发前不渲染子组件:
export function AuthProvider({ children }) { const [currentUser, setCurrentUser] = useState(); // 新增加载状态,默认true代表鉴权还在初始化 const [loading, setLoading] = useState(true); const dispatch = useDispatch(); // ... 省略signup、login、googleSignIn、logout原有方法 useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setCurrentUser(currentUser); // 鉴权状态获取完成,关闭加载态 setLoading(false); }); return unsubscribe; }, []); const value = { currentUser, signup, login, logout, googleSignIn, }; // 加载中时不渲染子路由,避免提前触发重定向 return <AuthContext.Provider value={value}> {loading ? <div>加载中...</div> : children} </AuthContext.Provider>; }
第二步:修改ProtectedRoute逻辑
鉴权加载过程中不做跳转判断,等加载完成后再校验用户状态:
import React from 'react'; import { Navigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; const ProtectedRoute = ({ children }) => { const { currentUser } = useAuth(); // 鉴权初始化完成前不做任何跳转操作 if (currentUser === undefined) { return <div>验证登录状态中...</div>; } if (!currentUser) { return <Navigate to="/" replace />; } return children; }; export default ProtectedRoute;
第三步:清理冗余代码
App.js里重复写了onAuthStateChanged监听逻辑,和AuthContext里的逻辑重复,可以删掉这部分冗余代码,统一用AuthContext维护登录状态即可。如果需要在App.js里调用路由相关钩子,只需要把App组件放在BrowserRouter内部,不要把BrowserRouter写在App组件里同时在App组件内调用路由钩子,就不会出现上下文不存在的报错。
改完之后刷新页面时,会先等待Firebase完成本地登录态校验,确认用户已登录就不会触发重定向,自然能停留在刷新前的页面。
内容的提问来源于stack exchange,提问作者Greg
相关产品推荐
相关产品推荐

