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

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鉴权状态初始化是异步过程:

  1. 页面刚加载时,AuthContext里的currentUser默认值是undefined,此时onAuthStateChanged的回调还没触发,还没拿到本地缓存的用户登录态
  2. ProtectedRoute在鉴权状态未确认时就做了判断,发现currentUser为空,直接执行重定向跳转到首页
  3. 等鉴权回调执行完成拿到用户信息时,已经完成跳转,自然无法停留在原页面

另外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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 02:24:16