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

解决React Suspense中Await导致函数重复执行的问题

React中Suspense+Await导致navigate/showAlert执行两次的解决方法

问题场景

在React中使用Suspense组件配合React-Router-DOM的Await组件等待loader返回的Promise解析时,遇到navigate、showAlert这类副作用函数被执行两次的问题。原始代码如下:

原始Loader代码

export const Loader = async ({ params }) => {
  CheckAuth();
  let fetch_data = fetchApi('auth/activate-account', 'POST', { verify_token: params.verify_token });
  return defer({ loaderPromise: fetch_data });
}
// 该Loader在Route的loader属性中调用

原始组件代码

const VerifyToken = () => {            
  const loaderData = useLoaderData();
            
  const navigate = useNavigate();
            
  const { showAlert } = useGeneral();
            
  return (
    <React.Suspense fallback={<PreLoader />}>
      <Await resolve={loaderData.loaderPromise}>
        {(resolvedData) => {
          if (resolvedData.type === "warning") {
            showAlert(resolvedData.type, resolvedData.msg);
            navigate('/Auth/Login', { replace: true });
            return null;
          }
          return (
            {/* 正常渲染的JSX */}
          );
        }}
      </Await>
    </React.Suspense>
  );
};

核心原因

  1. React严格模式(开发环境)会对组件渲染、回调函数进行双重调用,用于检测潜在的副作用问题,而Await的回调属于渲染阶段执行的函数,因此会被触发两次。
  2. Suspense的重新渲染逻辑也可能导致回调重复执行。

解决方案

方案一:将副作用逻辑移到Loader中(推荐)

把跳转、弹窗这类副作用从组件渲染阶段移到路由的loader中处理,符合React Router的设计规范,从根源避免重复执行。

修改后的Loader代码

export const Loader = async ({ params }) => {
  CheckAuth();
  const resolvedData = await fetchApi('auth/activate-account', 'POST', { verify_token: params.verify_token });
  
  if (resolvedData.type === "warning") {
    // 将弹窗信息存入sessionStorage,供组件读取
    sessionStorage.setItem('alertData', JSON.stringify({
      type: resolvedData.type,
      msg: resolvedData.msg
    }));
    // 抛出重定向,由React Router直接处理跳转
    throw redirect({
      pathname: '/Auth/Login',
      replace: true
    });
  }
  
  return defer({ loaderPromise: Promise.resolve(resolvedData) });
}

修改后的组件代码

const VerifyToken = () => {            
  const loaderData = useLoaderData();
  const { showAlert } = useGeneral();
  
  // 组件挂载时读取弹窗信息并显示
  React.useEffect(() => {
    const alertData = sessionStorage.getItem('alertData');
    if (alertData) {
      const { type, msg } = JSON.parse(alertData);
      showAlert(type, msg);
      sessionStorage.removeItem('alertData');
    }
  }, [showAlert]);
            
  return (
    <React.Suspense fallback={<PreLoader />}>
      <Await resolve={loaderData.loaderPromise}>
        {(resolvedData) => {
          return (
            {/* 正常渲染的JSX */}
          );
        }}
      </Await>
    </React.Suspense>
  );
};

方案二:用useEffect包裹副作用逻辑

如果不想修改loader,可以将Await回调中的副作用移到useEffect中,利用状态存储解析后的数据,确保副作用只执行一次。

const VerifyToken = () => {            
  const loaderData = useLoaderData();
  const navigate = useNavigate();
  const { showAlert } = useGeneral();
  const [resolvedData, setResolvedData] = React.useState(null);
  
  // 处理副作用逻辑
  React.useEffect(() => {
    if (resolvedData?.type === "warning") {
      showAlert(resolvedData.type, resolvedData.msg);
      navigate('/Auth/Login', { replace: true });
    }
  }, [resolvedData, navigate, showAlert]);
            
  return (
    <React.Suspense fallback={<PreLoader />}>
      <Await resolve={loaderData.loaderPromise}>
        {(data) => {
          setResolvedData(data);
          // 仅渲染正常状态的JSX
          if (data.type !== "warning") {
            return (
              {/* 正常渲染的JSX */}
            );
          }
          return null;
        }}
      </Await>
    </React.Suspense>
  );
};

方案三:优化useRef的实现方式

如果坚持使用原有的useRef方案,可以添加组件卸载时的清理逻辑,避免状态残留影响后续页面访问。

const VerifyToken = () => {            
  const loaderData = useLoaderData();
  const navigate = useNavigate();
  const { showAlert } = useGeneral();
  const hasExecuted = React.useRef(false);
  
  // 组件卸载时重置ref状态
  React.useEffect(() => {
    return () => {
      hasExecuted.current = false;
    };
  }, []);
            
  return (
    <React.Suspense fallback={<PreLoader />}>
      <Await resolve={loaderData.loaderPromise}>
        {(resolvedData) => {
          if (resolvedData.type === "warning" && !hasExecuted.current) {
            hasExecuted.current = true;
            showAlert(resolvedData.type, resolvedData.msg);
            navigate('/Auth/Login', { replace: true });
            return null;
          }
          return (
            {/* 正常渲染的JSX */}
          );
        }}
      </Await>
    </React.Suspense>
  );
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 10:47:07