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
问题原因分析
- 路由守卫依赖本地存储,未订阅Redux状态:
ProtectedUserRoute和AnonymousUserRoute直接读取localStorage的token,Redux状态更新后不会触发这两个组件重渲染,导致登录成功后路由守卫无法即时感知状态变化。 - 未等待异步登录请求完成就跳转:
handleLoginClick中同步调用navigate('/'),此时登录请求还在处理中,token未存入localStorage、Redux登录状态也未更新,跳转被路由守卫拦截。 - 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
相关产品推荐
相关产品推荐

