React Chat(MERN)应用ProtectedRoute竞态条件问题及优化需求
问题:MERN栈React聊天应用中ProtectedRoute的竞态条件问题
我基于MERN栈开发React聊天应用,给需要鉴权的路由实现了ProtectedRoute组件,但该组件和checkAuthAction、登录流程存在竞态条件。每次手动修改浏览器URL时,会触发后端checkAuth接口验证JWT有效性,后端逻辑正常,但AbortController提前中止导致问题。
现有代码
ProtectedRoute组件
import React, { useEffect } from 'react'; import { Navigate, useLocation } from 'react-router'; import { useAuth } from '../../store/auth'; import { ServiceStatus } from '../../constants'; import { checkAuthAction } from '../../store/auth/actions'; import { Loading } from '../loading'; export const ProtectedRoute = ({ children, }: { children: React.ReactElement }): React.ReactElement => { const { state: { isAuthenticated, serviceStatus }, dispatch, } = useAuth(); const location = useLocation(); useEffect(() => { const checkAuthAbortController = new AbortController(); const checkAuth = async () => { await checkAuthAction(dispatch, { signal: checkAuthAbortController.signal, }); }; checkAuth(); return () => { checkAuthAbortController.abort(); }; }, []); return ( <> {serviceStatus === ServiceStatus.Loading ? ( <Loading /> ) : isAuthenticated ? ( children ) : ( <Navigate to="/login" replace state={{ from: location }} /> )} </> ); };
checkAuthAction
import { checkAuth } from '../../../../services'; import React from 'react'; import { Actions } from '../../reducer.ts'; import { ActionType } from '../../action-type.ts'; import { ServiceStatus } from '../../../../constants'; import { AuthUser } from '../../store-types.ts'; export const checkAuthAction = async ( dispatch: React.Dispatch<Actions>, options: Record<string, unknown>, ): Promise<void> => { dispatch({ type: ActionType.CheckingAuth, payload: { serviceStatus: ServiceStatus.Loading, }, }); try { const checkAuthResponse: AuthUser = await checkAuth(options); console.log('Check Auth Response: ', checkAuthResponse); dispatch({ type: ActionType.CheckingAuthFinished, payload: { user: checkAuthResponse, }, }); } catch (error) { dispatch({ type: ActionType.CheckingAuth, payload: { serviceStatus: ServiceStatus.Error, }, }); console.log('Error: ', error); } };
竞态条件解决办法
1. 修正AbortController的触发逻辑与错误处理
当前问题核心在于:组件卸载时中止请求,但旧请求的错误回调仍会更新状态,干扰新路由的验证流程。需做以下调整:
- 给
useEffect添加location依赖,确保路由变化时重新发起验证并中止旧请求 - 在
checkAuthAction中过滤中止错误,避免错误状态污染
修改后的ProtectedRoute
import React, { useEffect } from 'react'; import { Navigate, useLocation } from 'react-router'; import { useAuth } from '../../store/auth'; import { ServiceStatus } from '../../constants'; import { checkAuthAction } from '../../store/auth/actions'; import { Loading } from '../loading'; export const ProtectedRoute = ({ children, }: { children: React.ReactElement }): React.ReactElement => { const { state: { isAuthenticated, serviceStatus }, dispatch, } = useAuth(); const location = useLocation(); useEffect(() => { const checkAuthAbortController = new AbortController(); const checkAuth = async () => { await checkAuthAction(dispatch, { signal: checkAuthAbortController.signal, }); }; checkAuth(); return () => { checkAuthAbortController.abort(); }; }, [location, dispatch]); // 添加路由和dispatch依赖 return ( <> {serviceStatus === ServiceStatus.Loading ? ( <Loading /> ) : isAuthenticated ? ( children ) : ( <Navigate to="/login" replace state={{ from: location }} /> )} </> ); };
修改后的checkAuthAction
import { checkAuth } from '../../../../services'; import React from 'react'; import { Actions } from '../../reducer.ts'; import { ActionType } from '../../action-type.ts'; import { ServiceStatus } from '../../../../constants'; import { AuthUser } from '../../store-types.ts'; export const checkAuthAction = async ( dispatch: React.Dispatch<Actions>, options: Record<string, unknown>, ): Promise<void> => { const signal = options.signal as AbortSignal; // 信号已中止则直接返回,避免无效请求 if (signal.aborted) return; dispatch({ type: ActionType.CheckingAuth, payload: { serviceStatus: ServiceStatus.Loading, }, }); try { const checkAuthResponse: AuthUser = await checkAuth(options); // 请求完成后再次检查信号,避免组件已卸载时更新状态 if (signal.aborted) return; console.log('Check Auth Response: ', checkAuthResponse); dispatch({ type: ActionType.CheckingAuthFinished, payload: { user: checkAuthResponse, }, }); } catch (error) { // 仅处理非中止类型的错误 if (!(error instanceof DOMException) || error.name !== 'AbortError') { dispatch({ type: ActionType.CheckingAuth, payload: { serviceStatus: ServiceStatus.Error, }, }); console.log('Error: ', error); } } };
2. 可选:添加请求标识实现状态隔离
给每个请求生成唯一ID,在reducer中只处理最新ID的action,彻底避免旧请求干扰状态:
// 在checkAuthAction中添加请求ID const requestId = Date.now(); dispatch({ type: ActionType.CheckingAuth, payload: { serviceStatus: ServiceStatus.Loading, requestId, }, }); // 在reducer中仅处理最新的requestId case ActionType.CheckingAuthFinished: if (action.payload.requestId === state.latestRequestId) { state.user = action.payload.user; state.isAuthenticated = true; }
业界规范的ProtectedRoute组件示例
React Router v6 标准实现(结合Redux Toolkit)
import React, { useEffect, useState } from 'react'; import { Navigate, useLocation, Outlet } from 'react-router-dom'; import { useAuth } from '../store/auth'; import Loading from '../components/Loading'; const ProtectedRoute = () => { const { isAuthenticated, isLoading, checkAuth } = useAuth(); const location = useLocation(); const [isValidationInitiated, setIsValidationInitiated] = useState(false); useEffect(() => { if (!isAuthenticated && !isLoading) { checkAuth().finally(() => setIsValidationInitiated(true)); } else { setIsValidationInitiated(true); } }, [isAuthenticated, isLoading, checkAuth]); // 未发起验证或验证中时显示加载态 if (!isValidationInitiated || isLoading) { return <Loading />; } return isAuthenticated ? ( <Outlet /> {/* 使用Outlet嵌套子路由,符合React Router v6规范 */} ) : ( <Navigate to="/login" replace state={{ from: location }} /> ); }; export default ProtectedRoute;
配套的Auth Store(Redux Toolkit)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { checkAuthApi } from '../services/api'; // 封装异步验证逻辑 export const checkAuth = createAsyncThunk( 'auth/checkAuth', async (_, { rejectWithValue }) => { try { const response = await checkAuthApi(); return response.data; } catch (error) { return rejectWithValue(error.response?.data || '验证失败'); } } ); const authSlice = createSlice({ name: 'auth', initialState: { user: null, isAuthenticated: false, isLoading: false, error: null, }, reducers: {}, extraReducers: (builder) => { builder .addCase(checkAuth.pending, (state) => { state.isLoading = true; state.error = null; }) .addCase(checkAuth.fulfilled, (state, action) => { state.user = action.payload; state.isAuthenticated = true; state.isLoading = false; }) .addCase(checkAuth.rejected, (state, action) => { state.isLoading = false; state.error = action.payload; state.isAuthenticated = false; }); }, }); export default authSlice.reducer;
内容的提问来源于stack exchange,提问作者Keyur Garsondiya
相关产品推荐
相关产品推荐

