Next.js登录跳转首页报Abort fetching component错误修复
问题根因分析
开发环境和生产环境的两个报错属于同一问题链触发,核心诱因共3个:
- 表单默认提交行为未被完全拦截:代码仅给
type="submit"的登录按钮绑定了onClick事件执行preventDefault(),但未给<form>标签本身绑定onSubmit处理逻辑。当用户按回车触发提交、或者点击按钮的事件冒泡顺序异常时,会触发表单原生GET提交,直接中断Next.js的客户端路由拉取流程,开发环境就会抛出Error: Abort fetching component for route: "/"。 - 响应解析逻辑无容错:
sendLoginData方法中未校验接口返回的HTTP状态、响应格式,直接调用response.json()。如果接口异常返回HTML格式的错误页(比如服务端500/404页、反向代理错误页),JSON解析时遇到HTML开头的<字符就会抛出语法错误;生产环境下路由跳转时未完成的请求被中断、静态资源加载失败返回HTML兜底页,也会触发Uncaught SyntaxError: Unexpected token '<'。 - 事件监听与DOM嵌套存在逻辑错误:
- 路由监听里
url.includes("/")的判断恒成立,所有路由跳转都会触发loading状态,和预期仅在跳首页时触发的逻辑不符 - 注册入口的
<Link>标签内部嵌套了带href=""的a标签,空href会覆盖Link的路由配置,点击时会触发当前页刷新,中断路由流程
- 路由监听里
修复方案
按以下顺序修改代码即可解决问题:
- 把表单提交逻辑绑定到form标签的
onSubmit事件上,移除提交按钮上的onClick绑定,从根源拦截原生表单提交 - 给fetch逻辑增加响应状态、格式校验,只有请求成功且返回格式为JSON时再做解析,确认登录成功后再执行路由跳转
- 修正路由监听的判断逻辑,修复Link标签的无效嵌套问题
- 增加loading状态的竞态处理,避免重复提交、组件卸载后更新状态
修正后的核心代码示例
const SigninForm = (props) => { const Router = useRouter(); const usernameRef = useRef(); const passwordRef = useRef(); const [isLoading, setIsLoading] = useState(false); const [captchaInput, setCaptchaInput] = useState(false); const [dataBaseResponse, setDataBaseResponse] = useState({}); const [fetchedData, setFetchedData] = useState(false); const [captchaCode, setCaptchaCode] = useState(); const sendLoginData = async () => { // 增加错误捕获和响应校验 try { const response = await fetch("/api/formDataSignIN", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id: usernameRef.current.value, password: passwordRef.current.value, captchaCode: captchaCode, }), }); // 先判断HTTP状态是否正常 if (!response.ok) throw new Error('Request failed'); // 判断响应类型是不是JSON,避免解析HTML报错 const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new Error('Invalid response format'); } const data = await response.json(); setDataBaseResponse({ message: data.message, data, }); // 登录成功后显式跳转,不要把跳转逻辑放到外部hook里隐式执行 if (data.state === true) { Router.push("/"); } return data; } catch (err) { setDataBaseResponse({ message: err.message, data: null }) return null; } }; // 把提交逻辑绑定到form的onSubmit上,不要绑定按钮onClick const handleLoginSendData = async (e) => { e.preventDefault(); if (isLoading) return; setIsLoading(true); await sendLoginData(); setFetchedData(true); setIsLoading(false); }; useNotification(dataBaseResponse, fetchedData, setFetchedData); useEffect(() => { const handleStart = (url) => { // 修正判断逻辑:只有跳根路径且当前不在首页时才触发loading if (url === "/" && window.location.pathname !== "/") { setIsLoading(true); } }; const handleStop = () => { setIsLoading(false); }; Router.events.on("routeChangeStart", handleStart); Router.events.on("routeChangeComplete", handleStop); Router.events.on("routeChangeError", handleStop); return () => { Router.events.off("routeChangeStart", handleStart); Router.events.off("routeChangeComplete", handleStop); Router.events.off("routeChangeError", handleStop); }; }, [Router]); const handleCaptchaValue = (e) => { if (e) { setCaptchaInput(true); setCaptchaCode(e); } }; return ( <Fragment> <Head> <title>Sign in</title> <meta name="description" content="Generated by create next app" /> </Head> <section className={style.section}> <div className={style.container}> <div className={cls(style.user, style.signinBx)}> <div className={style.imgBx}> <Image src="/meta2.jpeg" alt="meta" layout="fill" /> </div> <div className={style.formBx}> {/* 在这里绑定onSubmit,拦截原生表单提交 */} <form onSubmit={handleLoginSendData}> <div style={{ marginBottom: "80px", }} > <Link href="/" shallow={true}> <a> <FontAwesomeIcon icon={faArrowLeft} /> </a> </Link> </div> <h2>Sign In</h2> <input type="text" name="email" ref={usernameRef} placeholder="email or phoneNumber" /> <input type="password" name="Password" ref={passwordRef} placeholder="Password" /> <Googlerecaptcha handleCaptchaValue={handleCaptchaValue} /> <div className={style.btnBx}> <LoginButton /> </div> <input type="submit" value={isLoading ? "Loading..." : "Login"} // 移除按钮上的onClick,提交逻辑统一交给form处理 disabled={isLoading || !captchaInput} /> <p className={style.signup}> Dont have an account ? {/* 移除a标签上的空href,避免覆盖Link的路由逻辑 */} <Link href="/signUp"> <a>Sign Up</a> </Link> </p> </form> </div> </div> </div> </section> </Fragment> ); }; export default SigninForm;
额外注意事项
- 不要在隐式执行的hook(比如当前使用的
useNotification)里做路由跳转,必须拿到登录接口的成功响应后再显式调用Router.push,避免接口还没返回就提前跳转中断请求 - 提交按钮增加
disabled状态,避免用户重复点击触发多次提交 - 如果生产环境部署在反向代理后面(比如Nginx),需要检查静态资源的路径配置,确保
_next/static目录下的资源可以被正常访问,否则资源加载404返回HTML也会触发<开头的语法错误
内容的提问来源于stack exchange,提问作者Arman Amini
相关产品推荐
相关产品推荐

