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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 04:19:58