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发送请求,不通过
useAxioshook,验证是否是拦截器逻辑导致的阻塞。如果纯请求正常,再逐步添加拦截器代码定位问题点。
内容的提问来源于stack exchange,提问作者Adrianwritenow
相关产品推荐
相关产品推荐

