Next.js API调用失败时如何展示自定义错误页面
问题现象
- 已完成Next.js自定义404错误页开发,预期API调用失败时触发展示,但实际API返回404时,页面与路由匹配成功时的渲染结构完全一致,仅缺失API返回的业务数据
- 复现规则:路由
companies/neimans可正常拉取API数据渲染企业内容,输入错误路径companies/neiman时,控制台可观测到404状态报错,但既不展示自定义404页,也不展示Next.js默认404页,仅渲染结构一致的空数据页面 - 项目结构:
pages目录下包含companies文件夹,内有动态路由文件[companydata].tsx(负责拉取API数据动态渲染企业信息)、自定义错误页404.tsx
现有实现代码
API请求方法
export const getCompanies = async (routeData: string): Promise<Company> => { const client = getApiClient(); const response = await client.get<Company>(`api/companies/${routeData}`); if (response) { return response.data; } return {} as Company; }
已尝试的失效方案
曾在[companydata].tsx中判断返回的企业数据对象是否为空,为空则重定向到404页面,代码如下:
if (Object.keys(company).length === 0) { return <Redirect to="/company/404"/>; }
方案问题:页面会始终展示404内容。通过日志打印发现页面会触发多次渲染,前6次渲染时数据为空对象,直到第6次渲染后接口数据才正式返回,导致404误触发,且未定位到多次提前渲染的原因。
数据请求封装
将getCompanies方法封装在getData函数中做统一并行调用,代码如下:
export const getData = async (companyName: string): Promise<[Company, Sales]> => { if (companyName) { return (await Promise.all([getCompanies(companyName), getSales()])) as [ Company, Sales ]; } return [{} as Company, {} as Sales]; };
页面组件逻辑
getData方法在[companydata].tsx的useEffect钩子中调用,对应代码如下:
const Company: NextPage = (): JSX.Element => { const [selectedCompany, setSelectedCompany] = useState<Company>({} as Company); const [salesAvailable, setSalesAvailable] = useState<boolean>(false); const [sales, setSales] = useState<Sales>({} as Sales); const router = useRouter(); const {companydata} = router.query; useEffect(() => { const init = async (companyName: string) => { const [companyData, salesData] = await getData(companyName); if (companyData) { setSelectedCompany(companyData); } if (salesData) { setSalesAvailable(true); setSales(salesData); } else { setSalesAvailable(false); } } init(companydata as string); }, [companydata]); // 此处返回企业页面渲染内容 }
问题根因
- Next.js动态路由
[companydata].tsx会匹配所有companies/*格式的路径,无论路径参数对应的企业是否存在,路由本身都会匹配成功,因此Next.js不会自动触发404页面 - 客户端侧
useEffect在组件首次挂载完成后才会执行,组件首次渲染时router.query在水合阶段未准备完成、接口请求也未返回,初始状态的selectedCompany为空对象,这就是多次提前渲染空数据的核心原因 - 现有
getCompanies方法未捕获接口错误:API返回404时请求客户端会直接抛出异常,方法未做错误捕获、也未识别404状态,仅默认返回空对象,没有区分「请求中」「请求成功有数据」「请求失败404」三种状态 - 之前的空对象判断逻辑未加加载态,在请求还未返回的加载阶段就判定为空跳转404,必然出现误触发。额外问题:通过重定向跳转到单独404路由会返回302状态码,不符合404语义,也不利于SEO。
修复方案
- 新增状态标识,区分请求中、请求成功、请求失败三种状态,避免加载阶段误判
// 组件内新增两个状态 const [loading, setLoading] = useState<boolean>(true); const [isNotFound, setIsNotFound] = useState<boolean>(false);
- 改造API请求方法,捕获404错误,不要无脑返回空对象
export const getCompanies = async (routeData: string): Promise<Company | null> => { const client = getApiClient(); try { const response = await client.get<Company>(`api/companies/${routeData}`); return response.data; } catch (err: any) { // 识别404状态返回null,其余错误可按需做全局异常处理 if (err.response?.status === 404) { return null; } throw err; } }
- 改造页面逻辑,等路由参数准备完成再发请求,请求过程中维持加载态,根据接口返回结果判断是否渲染404
const Company: NextPage = (): JSX.Element => { const [selectedCompany, setSelectedCompany] = useState<Company | null>(null); const [salesAvailable, setSalesAvailable] = useState<boolean>(false); const [sales, setSales] = useState<Sales>({} as Sales); const [loading, setLoading] = useState<boolean>(true); const [isNotFound, setIsNotFound] = useState<boolean>(false); const router = useRouter(); const {companydata} = router.query; useEffect(() => { // 路由参数未就绪时不触发请求 if (!companydata) return; const init = async (companyName: string) => { try { setLoading(true); const [companyData, salesData] = await getData(companyName); // 接口返回null代表企业不存在,标记404状态 if (!companyData) { setIsNotFound(true); return; } setSelectedCompany(companyData); setSalesAvailable(!!salesData); if (salesData) setSales(salesData); } finally { setLoading(false); } } init(companydata as string); }, [companydata]); // 加载阶段渲染加载占位,不做数据判空 if (loading) return <div>加载中...</div> // 识别到404时直接返回自定义404组件,无需路由重定向 if (isNotFound) return <NotFound />; // 正常渲染企业页面内容 return <div>{/* 企业信息展示逻辑 */}</div> }
- SEO优化方案:如果需要返回正确的404状态码,可将数据请求迁移到
getServerSideProps中,在服务端识别404后返回notFound: true,Next.js会自动渲染自定义404页面,同时返回标准404状态码
export async function getServerSideProps(context) { const { companydata } = context.params; try { const companyData = await getCompanies(companydata as string); if (!companyData) { return { notFound: true } } // 其余数据拉取逻辑 return { props: { companyData /* 其余透传页面数据 */ } } } catch (err) { return { notFound: true } } }
内容的提问来源于stack exchange,提问作者dev_in_training
相关产品推荐
相关产品推荐

