Next.js App Router 约定式文件实战:loading、error、not-found 怎么兜住加载态与异常

Next.js App Router 约定式文件实战:loading、error、not-found 怎么兜住加载态与异常

用 Next.js App Router 写页面,你迟早会遇到这三个问题:

  1. 页面里 await fetch(...) 拉数据时,用户盯着一片空白,不知道是在加载还是卡死了。
  2. 接口挂了或抛异常,整个页面直接白屏崩掉,还可能把错误堆栈暴露给用户。
  3. 用户访问了一个不存在的资源(比如 /posts/99999),你想给个像样的 404 页,而不是默认那个丑页面。

很多人第一反应是在组件里手写 isLoadingtry/catchif (!data) return <NotFound/>。但 App Router 提供了约定式文件 ------在路由目录里放特定文件名,框架自动帮你接管加载态、错误态和 404。这篇把 loading.tsxerror.tsxnot-found.tsx 三个一次讲透。

朴素写法:所有状态挤在一个组件里

先看不用约定式文件时,一个详情页大概长这样:

tsx 复制代码
// app/posts/[id]/page.tsx ------ 反面教材
'use client';
import { useEffect, useState } from 'react';

export default function PostPage({ params }: { params: { id: string } }) {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(`/api/posts/${params.id}`)
      .then(r => { if (!r.ok) throw new Error('加载失败'); return r.json(); })
      .then(setPost)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [params.id]);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了</p>;
  if (!post) return <p>找不到文章</p>;
  return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}

问题很明显:业务渲染只有最后一行,前面全是状态判断的噪音;而且被迫写成客户端组件('use client'),丢掉了服务端组件直接 await 数据的能力。App Router 的约定式文件就是来拆掉这些样板的。

loading.tsx:自动加载态,基于 Suspense

在路由目录放一个 loading.tsx,Next.js 会自动用它包一层 Suspense。当同级 page.tsx(服务端组件)在 await 数据时,先渲染 loading.tsx 的内容,数据好了再换成真实页面:

tsx 复制代码
// app/posts/[id]/loading.tsx
export default function Loading() {
  // 骨架屏,比"加载中..."体验好得多
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 w-2/3 bg-gray-200 rounded" />
      <div className="h-4 w-full bg-gray-200 rounded" />
      <div className="h-4 w-5/6 bg-gray-200 rounded" />
    </div>
  );
}

有了它,page.tsx 就能回归纯粹的服务端组件,直接 await,不用管 loading:

tsx 复制代码
// app/posts/[id]/page.tsx ------ 服务端组件,直接 await
export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;   // Next.js 15 起 params 是 Promise
  const res = await fetch(`https://api.example.com/posts/${id}`);
  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

关键理解 :loading.tsx 的本质就是给 page.tsx 套了 <Suspense fallback={<Loading/>}>。所以它对「路由跳转时的加载」和「组件 await 期间」都生效,你什么都不用手写。

error.tsx:兜住渲染异常,必须是客户端组件

如果 page.tsx 里的 fetch 抛了异常,或者渲染过程报错,同级的 error.tsx 会接住它,展示降级 UI,而不是整页白屏:

tsx 复制代码
// app/posts/[id]/error.tsx
'use client';  // ← error.tsx 必须是客户端组件,这是硬性要求

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;   // 调它会重新渲染这段路由,相当于"重试"
}) {
  useEffect(() => {
    // 上报到监控系统,别把错误吞了
    console.error('页面渲染出错:', error);
  }, [error]);

  return (
    <div className="p-6 text-center">
      <h2>😵 出了点问题</h2>
      <p className="text-gray-500">{error.message}</p>
      <button
        onClick={() => reset()}
        className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
      >
        重试
      </button>
    </div>
  );
}

有两个坑要记牢:

坑一:error.tsx 必须加 'use client' 它内部用 React 错误边界实现,错误边界只能是客户端组件。忘了加,构建直接报错。

坑二:error.tsx 抓不到同级 layout.tsx 的错误。 因为 error 边界包在 layout 内部,layout 本身抛错它管不着。要兜住 layout 的错误,得在上一级 目录放 error.tsx,或者用 global-error.tsx 兜住根 layout。

reset() 函数是 App Router 特有的:调用它会尝试重新渲染出错的这段路由子树,给用户一个「重试」而不用刷新整页。

not-found.tsx + notFound():优雅的 404

资源不存在时,别手动 return <div>404</div>。App Router 提供了 notFound() 函数,调用它会立即中断渲染,并渲染最近的 not-found.tsx:

tsx 复制代码
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation';

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const res = await fetch(`https://api.example.com/posts/${id}`);

  if (res.status === 404) {
    notFound();   // ← 中断渲染,跳到 not-found.tsx,后面代码不会执行
  }

  const post = await res.json();
  return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}
tsx 复制代码
// app/posts/[id]/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div className="p-6 text-center">
      <h2>文章不存在</h2>
      <p className="text-gray-500">你要找的文章可能已被删除。</p>
      <Link href="/posts" className="text-blue-600 underline">
        返回文章列表
      </Link>
    </div>
  );
}

notFound() 的原理是抛出一个特殊错误,被框架捕获后渲染 not-found.tsx,同时响应状态码正确返回 404 ------这点对 SEO 很重要,搜索引擎知道这是不存在的页面,不会收录。手写 <div>404</div> 的话状态码还是 200,搜索引擎会以为是正常页。

它们的嵌套关系:一张图理清

这几个约定式文件不是平级的,它们的包裹顺序是固定的。同一个路由段里,Next.js 大致这样嵌套:

text 复制代码
<layout>
  <error>            ← 兜住下面的渲染错误
    <suspense fallback={<loading>}>   ← loading 兜住加载态
      <not-found 边界>
        <page>       ← 你的实际页面
      </not-found>
    </suspense>
  </error>
</layout>

从这张图能推出几条实用结论:

  • loadingerror 都定义在路由段上,子路由可以有自己的一套,就近生效。
  • errorlayout 内层,所以管不到同级 layout 的错误(要放上一级)。
  • not-found 既能被 notFound() 主动触发,也会兜住未匹配的路由。

小结

App Router 的三个约定式文件,把过去挤在组件里的状态处理拆成了框架职责:

  • loading.tsx :自动 Suspense fallback,让 page.tsx 回归纯服务端组件直接 await,写骨架屏体验最佳。
  • error.tsx :错误边界,必须 'use client' ;提供 reset() 做重试;抓不到同级 layout 的错误,要放上一级。
  • not-found.tsx + notFound() :优雅 404,且返回正确的 404 状态码 ,对 SEO 友好;notFound() 会中断后续渲染。

记忆点:别再手写 isLoading / try-catch / 404 判断------在路由目录里放对文件名,框架自动帮你套好 Suspense 和错误边界。

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱16 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家16 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端16 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
大龄秃头程序员16 小时前
iOS 客户端视角扫盲:WKWebView 里 window、messageHandlers 与 Native 回调到底怎么工作?
javascript
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue16 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
春生野草16 小时前
个人笔记——C语言字符串、树
c语言·开发语言·笔记
风中芦苇啊16 小时前
Java EasyExcel 导入通用工具类:自定义注解映射字段 + 反射机制
java·开发语言