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

React中登录按钮点击后受保护路由无法即时跳转的解决方法

问题描述

点击登录按钮的handleLoginClick方法后,navigate('/')无法立即跳转到首页,推测是ProtectedUserRoute的路由拦截导致;但刷新页面后,路由能识别本地存储中的token并正常跳转到首页。需要实现无需刷新的即时跳转。

现有代码文件

1. App.js

<Router>
     <Routes>
         <Route element={<AnonymousUserRoute />}>
              <Route path='/user/login' element={<UserLoginScreen />}/>
         </Route>
         <Route element={<ProtectedUserRoute />} >
              <Route path='/' element={<HomeScreen />} exact />
         </Route>
     </Routes>
</Router>

2. ProtectedUserRoute.js

function ProtectedUserRoute() {
    const loginPageLink = "/user/login";
    const token = //getting from local storage

    return (
        token ? <Outlet /> : <Navigate to={loginPageLink} replace />
    );
};

3. AnonymousUserRoute.js

function AnonymousUserRoute() {
    const homePageLink = "/";
    const token = //getting from local storage

    return (
        token ? <Navigate to={homePageLink} replace /> : <Outlet />
    );
};

4. Login.js

function UserLoginScreen() {
    ...
    const handleLoginClick = (event) => {
        event.preventDefault();
        dispatch(loginUser(email, password));
        navigate('/');
    }
    ...
}

5. reducers.js

export const userLoginReducer = (state = { userDetails: { isLoggedIn: false } }, action) => {
    switch(action.type) {
        case USER_LOGIN_REQUEST:
            return { loading: true }
        case USER_LOGIN_SUCCESS:
            return { loading: false, isLoggedIn: action.payload }
        case USER_LOGIN_FAIL:
            return { loading: false, error: action.payload }
        default:
            return state
    }
}

6. actions.js

export const loginUser = (email, password) => async (dispatch) => {
    const loginUrl = //loginUrl
    try {
        dispatch({ type: USER_LOGIN_REQUEST })
        
        const loginData = { email: email, password: password }
        const config = { headers: { 'Content-type': 'application/json'} }
        const { data } = await axios.post(loginUrl, loginData, config)
        
        dispatch({ type: USER_LOGIN_SUCCESS, payload: true })
        
        storeUserLoggedInDetailsInLocalStorage(data.tokens); //calling a function which stores in localstorage. Nothing else done here.
    } catch(error) {
        dispatch({
            type: USER_LOGIN_FAIL,
            payload: error.response && error.response.data.detail
                    ? error.response.data.detail
                    : error.message
        })
        console.log(error)
    }
}

7. store.js

const userDetailsFromStorage = localStorage.getItem('userDetails') ? 
                         JSON.parse(localStorage.getItem('userDetails')) : {}

const reducer = combineReducers({
    userDetails: userLoginReducer,
})

const persistedState = {
    userDetails: {
        isLoggedIn: userDetailsFromStorage.isLoggedIn ?? false
    }
}

const middleWare = [thunk]

const store = createStore(reducer, persistedState, composeWithDevTools(applyMiddleware(...middleWare)));

export default store

问题原因分析

  1. 路由守卫依赖本地存储,未订阅Redux状态:ProtectedUserRoute和AnonymousUserRoute直接读取localStorage的token,Redux状态更新后不会触发这两个组件重渲染,导致登录成功后路由守卫无法即时感知状态变化。
  2. 未等待异步登录请求完成就跳转:handleLoginClick中同步调用navigate('/'),此时登录请求还在处理中,token未存入localStorage、Redux登录状态也未更新,跳转被路由守卫拦截。
  3. Redux reducer状态结构不一致:reducer初始状态包含userDetails嵌套,但登录成功后返回的状态无该嵌套,导致useSelector获取状态时结构混乱,无法正确读取isLoggedIn。

解决方案及代码修改

1. 修正Redux reducer的状态结构

确保reducer状态结构始终一致:

// reducers.js
export const userLoginReducer = (state = { loading: false, isLoggedIn: false, error: null }, action) => {
    switch(action.type) {
        case USER_LOGIN_REQUEST:
            return { ...state, loading: true, error: null }
        case USER_LOGIN_SUCCESS:
            return { ...state, loading: false, isLoggedIn: action.payload }
        case USER_LOGIN_FAIL:
            return { ...state, loading: false, error: action.payload }
        default:
            return state
    }
}

2. 让路由守卫订阅Redux的登录状态

修改路由守卫组件,通过useSelector获取Redux中的isLoggedIn状态,状态更新时自动重渲染:

// ProtectedUserRoute.js
import { useSelector } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';

function ProtectedUserRoute() {
    const loginPageLink = "/user/login";
    const { isLoggedIn } = useSelector(state => state.userDetails);

    return isLoggedIn ? <Outlet /> : <Navigate to={loginPageLink} replace />;
};
// AnonymousUserRoute.js
import { useSelector } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';

function AnonymousUserRoute() {
    const homePageLink = "/";
    const { isLoggedIn } = useSelector(state => state.userDetails);

    return isLoggedIn ? <Navigate to={homePageLink} replace /> : <Outlet />;
};

3. 等待异步登录请求完成后再跳转

修改登录页面的点击事件,等待登录action执行完成后再跳转:

// Login.js
function UserLoginScreen() {
    ...
    const handleLoginClick = async (event) => {
        event.preventDefault();
        try {
            // 等待登录请求完成
            await dispatch(loginUser(email, password));
            // 登录成功后再跳转
            navigate('/');
        } catch (error) {
            // 处理登录失败逻辑,比如提示用户错误信息
            console.error('登录失败:', error);
        }
    }
    ...
}

4. 修正本地存储的登录状态

确保本地存储同时保存isLoggedIn状态,保证页面刷新时Redux能正确初始化:

// 假设该函数在actions.js或其他文件中
const storeUserLoggedInDetailsInLocalStorage = (tokens) => {
    localStorage.setItem('userDetails', JSON.stringify({
        tokens,
        isLoggedIn: true
    }));
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 12:22:05