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

Next.js 12.3页面Axios请求触发标签页冻结,求排查方向

问题描述

在Next.js 12.3项目中,使用Axios拦截器hook处理内部请求,该拦截器在多数页面正常工作,但在路径/me/settings/password&security/email/verify的页面中,提交操作会导致标签页冻结。请求卡在预检阶段数分钟后,出现[Violation] 'submit' handler took <N>ms提示。相同请求在其他页面(如/me/settings/password&security/email)可正常执行,即使将正常页面的完整代码复制到该问题页面,冻结问题仍存在。

页面组件代码

export default function VerifyEmailForm() {
  const router = useRouter();

  const user: UserState = useSelector((root: RootState) => root.user);
  const [apiError, setApiError] = useState("");
  const [schema, setSchema] = useState<any>();
  const [resubmit, setResubmit] = useState(false);
  const [updateEmail, setUpdateEmail] = useState(false);
  const [submitSuccsess, setSubmitSuccess] = useState(false);

  const [dispatchAxios, { success, loading, error }] = useAxios();

  const initialValues = {
    email: user?.email ? user?.email : "",
  };

  const handleSubmit = async (values: any) => {
    if (!user.email || updateEmail) {
      await dispatchAxios(
        updateUser({
          action: [
            {
              key: "email",
              value: values?.email,
            },
          ],
        })
      );
    }
    await dispatchAxios(requestEmailVerification(values?.email)).then((res) => {
      if (res?.data?.error) {
        setApiError(res?.data?.error);
      } else {
        setResubmit(true);
        setSubmitSuccess(true);
      }
    });
  };

  useEffect(() => {
    const yup = import("yup").then((yup) =>
      setSchema(
        yup.object().shape({
          email: yup
            .string()
            .email("Must be a valid email")
            .required("Email is required"),
        })
      )
    );
  }, []);

  useEffect(() => {
    if (error && !loading) {
      setApiError(error?.message);
    }
  }, [loading, success, error]);

  return (
    <Formik
      validationSchema={schema}
      initialValues={initialValues}
      onSubmit={handleSubmit}
      validateOnBlur={true}
    >
      {({ handleSubmit, setFieldValue, errors, values, touched, isValid }) => {
        return (
          <Form
            onSubmit={handleSubmit}
            onBlur={() =>
              setTimeout(() => {
                setApiError("");
              }, 200)
            }
          >
            <div className="flex flex-wrap gap-9 pb-2.5  ">
              {!user.email || updateEmail ? (
                <div className="w-full relative">
                  <div className="relative w-full">
                    <Field
                      styles="bg-transparent text-center"
                      name="email"
                      type="email"
                      placeholder="Email address"
                      component={InputField}
                      error={errors.email}
                    />
                    {values.email && (
                      <button
                        type="button"
                        onClick={() => setFieldValue("email", "")}
                        className={`flex items-center justify-center bg-background-secondary p-0.5  rounded-full w-4 h-4   absolute bottom-2  ${
                          errors.email ? "right-8 " : "right-2.5 "
                        }`}
                      >
                        <span className="sr-only">Clear Field</span>
                        <XMarkIcon
                          className="text-deactive w-2.5 "
                          aria-hidden="true"
                        />
                      </button>
                    )}
                  </div>
                  <InputError
                    error={errors.email as string}
                    touched={touched?.email as boolean}
                    styles={"flex justify-center"}
                  />
                </div>
              ) : (
                <></>
              )}

              <div className="w-full flex flex-col justify-center items-center">
                <Button
                  type="submit"
                  disabled={!isValid}
                  classes={`w-full text-white p-3  rounded-2xl  mx-8 ${
                    errors.email || !values.email.length
                      ? "border-transparent text-deactive"
                      : "border-2 border-primary"
                  }`}
                  label={resubmit ? "Resend Verification" : "Verify"}
                />
                {user.email && (
                  <Button
                    type="button"
                    classes={`w-full text-white p-3  rounded-2xl mx-8 border-transparent`}
                    label="Update Email"
                    onClick={() => setUpdateEmail(true)}
                  />
                )}
                <Button
                  type="button"
                  classes={`w-full text-white p-3  rounded-2xl mx-8 border-transparent`}
                  label="Cancel"
                  onClick={() => router.back()}
                />
              </div>
            </div>
            <ErrorAlertModal errors={apiError ? [apiError] : undefined} />
            <SuccessAlertModal success={submitSuccsess} />
          </Form>
        );
      }}
    </Formik>
  );
}

Axios Hook代码

export function useAxios(
  internal: boolean = true
): [DispatchAxios, IAxiosState] {
  const { data: session, status } = useSession();
  const dispatch = useDispatch();
  const isMounted = useRef(true);
  const user: UserState = useSelector((root: RootState) => root.user);
  const env = process.env.NODE_ENV;

  const { logoutUser } = useLogout();

  const client = axios.create({
    //   baseURL: process.env.API_URL,
  });

  client.interceptors.request.use((config: AxiosRequestConfig) => {
    config.headers = config.headers || {};

    if (!config.headers["Content-Type"]) {
      config.headers["Content-Type"] = "application/json";
    }

    if (!config.headers["Ocp-Apim-Subscription-Key"] && env === "production") {
      config.headers[
        "Ocp-Apim-Subscription-Key"
      ] = `${process.env.NEXT_PUBLIC_API_GATEWAY_KEY}`;
    }

    // Internal requests need tokens and other parameters added in.
    if (internal) {
      if (session?.accessToken) {
        config.headers.Authorization = `Bearer ${session.accessToken}`;
      }
    }

    return config;
  });

  const [state, setState] = useState<IAxiosState>({
    loading: false,
    error: null,
    response: null,
    success: false,
  });

  const dispatchAxios: DispatchAxios = useCallback(
    (params: IAxiosAction) => {
      const action: IAxiosAction = {
        type: params.type,
        config: params.config,
        batchOrder: params.batchOrder || [],
      };

      if (params.data) {
        action.data = params.data;
      }

      dispatch({
        ...action,
        loading: true,
      });

      setState({
        ...state,
        error: null,
        response: null,
        loading: true,
        success: false,
      });

      return client(params.config)
        .then((response: AxiosResponse) => {
          // dispatch must come before setState

          if (response.data.err === "INVALID TOKEN") {
            throw action;
          }

          dispatch({
            ...action,
            response,
            loading: false,
          });

          if (isMounted.current) {
            setState({
              ...state,
              loading: false,
              success: true,
              error: null,
              response,
            });
          }
          return {
            success: true,
            error: null,
            response,
          };
        })
        .catch((error: AxiosError) => {
          const originalReq = error.config;

          // Token refresh failed, log user out.
          if (
            originalReq?.url ===
            process.env.NEXT_PUBLIC_AUTH_REFRESH_API_ENDPOINT
          ) {
            // logoutUser();
          }
          // dispatch must come before setState
          dispatch({
            ...action,
            error,
            loading: false,
            success: false,
          });

          if (isMounted.current) {
            setState({
              ...state,
              loading: false,
              success: false,
              response: null,
              error,
            });
          }

          return {
            success: false,
            response: null,
            error,
          };
        });
    },

    [isMounted, dispatch, state, client]
  );

  useEffect(() => {
    isMounted.current = true;
    return () => {
      isMounted.current = false;
    };
  }, [state]);

  return [dispatchAxios, state];
}
排查调试方向
  • 检查路由路径特殊字符:当前路径包含&,可能导致Next.js路由解析或浏览器处理异常。尝试将路径中的&替换为-或其他合法字符,测试是否解决冻结问题。
  • 预检请求(OPTIONS)阻塞排查:在浏览器DevTools的Network面板中筛选OPTIONS请求,对比正常页面和问题页面的请求差异,重点检查Origin、Access-Control-*相关响应头是否正确返回,确认是否存在CORS配置问题。
  • 修复Axios实例重复创建:useAxios中每次组件渲染都会重新创建axios.create()实例,可能导致拦截器重复注册或请求上下文异常。将client的创建移到useMemo中避免重复初始化:
    const client = useMemo(() => axios.create({
      // baseURL: process.env.API_URL,
    }), []);
    
  • 解决状态更新循环依赖:dispatchAxios的useCallback依赖包含state,而setState又会更新state,可能形成循环。将setState改为函数式更新,移除对当前state的依赖:
    setState(prev => ({
      ...prev,
      error: null,
      response: null,
      loading: true,
      success: false,
    }));
    
    同时调整useCallback的依赖列表,移除state。
  • 优化提交事件异步处理:Formik的onSubmit如果未正确处理异步逻辑,可能阻塞主线程。在handleSubmit中使用Formik提供的setSubmitting控制提交状态,避免阻塞:
    const handleSubmit = async (values: any, { setSubmitting }) => {
      setSubmitting(true);
      try {
        // 原有请求逻辑
      } finally {
        setSubmitting(false);
      }
    };
    
  • 修正组件挂载状态逻辑:useAxios中isMounted的useEffect依赖state,会在每次状态更新时重置isMounted.current。将useEffect的依赖改为空数组,确保只在组件挂载和卸载时执行:
    useEffect(() => {
      isMounted.current = true;
      return () => {
        isMounted.current = false;
      };
    }, []);
    
  • 测试纯Axios请求:在问题页面中直接使用原生Axios发送请求,不通过useAxios hook,验证是否是拦截器逻辑导致的阻塞。如果纯请求正常,再逐步添加拦截器代码定位问题点。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 21:35:18