昨天介紹了使用 loading.tsx
和 <Suspense>
可以在頁面或某個 components 還沒載入完成時,頁面先顯示一個替代的 UI,像是 loading...。
那假如載入時遇到錯誤,會發生什麼事?
比方說,我讓 Server Component <ProductList>
等一個 2 秒後 resolve 的 Prmoise 處理完後,throw 一個 error:
export default async function ProductList() {
async function getProducts() {
await new Promise((resolve) => setTimeout(resolve, 2000));
throw new Error('Test Error');
}
來看看 Promise 處理完後會發生什麼事:
會直接白頁,因為當 render 遇到問題時,React 預設會直接移除頁面 UI。
所以在寫 React app 的時候,為了防止某個 component 錯誤造成整頁白頁,我們會在 component 外層包一個 error boundary component,讓這個 component 假入渲染遇到問題時,可以顯示一個替代的 UI ( fallback UI ),而不是讓整頁白頁。以下方 sample code 為例,當 <Profile>
渲染發生錯誤時,畫面會顯示 Something went wrong。
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<Profile />
</ErrorBoundary>
小提醒:目前 error boundary component 只能使用 class component,比方說:
'use client';
import React, { ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.log(error, info.componentStack);
}
render(): ReactNode {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
假如習慣使用 functional component 的朋友可以使用 react-error-boundary 這個 npm 套件。
那假如我想針對 route 做 error boundary 呢?App Router 提供了一個特殊檔案 - error.tsx
,比方說我可以在 app/products 中新增一個 error.tsx
:
/* app/products/error.tsx */
'use client';
export default function Error() {
return (
<div className='absolute top-1/2 left-1/2 translate-x-[-50%] font-bold text-[30px]'>
發生問題,請稍後再試
</div>
);
}
這時 Next 會在 Page 外包一個 error boundary component,而 error.tsx
中定義的 UI 就會是 error boundary 的 fallback UI。
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/error-handling)
以上述例子來說,當 /products/page.tsx
渲染發生錯誤時,頁面中間就會顯示「發生問題,請稍後再試」,而 layout ( header 和 footer ) 依然會存在頁面,且 layout 中的 state 也不會被重置。
假如不知道什麼是 layout,可以參考 Day 09 的文章。
這邊要注意,Error 只能是一個 Client Component,所以記得要在 error.tsx
標記 'use client'
。
Error 可以帶兩個 props - error 和 reset。
error 主要紀載錯誤訊息,我們可以使用 error.message
來取得錯誤訊息。假如錯誤發生在 Server Components,安全起見,Next 會避免在 error.message
中顯示出較敏感的資訊 ( ex: 用戶資料 ),這時我們可以透過 error.digest
來取得該 error 在 server logs 的 hash,去檢查錯誤發生原因。
而 reset 則是 Next 提供的一個類似「再試一次」的功能,可以在不影響到其他 component 的狀態下,讓發生錯誤的 component re-render。
假如 re-render 後沒有發生錯誤,則 Error component 就會被 re-render 的 UI 取代:
/* app/products/error.tsx */
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
假如使用 error.tsx
,我們是將 error boundary 設在整個 Page component。所以當商品列表渲染發生錯誤時,「強檔商品特賣中」也會被 fallback UI 覆蓋掉。
假如我希望商品列表渲染發生錯誤時,「強檔商品特賣中」依然存在呢?可以把 error boundary 設在 <ProductList>
:
/* app/products/page.tsx */
import { Suspense } from 'react';
import ErrorBoundary from './components/ErrorBoundary';
import Loading from './components/Loading';
import ProductList from './components/ProductList';
export default function Page() {
return (
<div className='mt-[100px]'>
<div className='w-full text-center font-bold text-[30px] mb-[50px]'>
強檔商品特賣中!
</div>
<ErrorBoundary fallback={<p>發生錯誤,請稍後再試</p>}>
<Suspense fallback={<Loading />}>
<ProductList />
</Suspense>
</ErrorBoundary>
</div>
);
}
假如沒有想讓 <ProductList>
作為 streaming boundary,可以把 <Suspense>
拿掉。
那假如我的專案中,有很多個 error.tsx
和 error boundary components,Next 會怎麼決定 component 渲染發生錯誤時,error boundary 要套用哪個呢?
當 component 渲染發生錯誤時,Next 會往 component tree 父層找最接近 component 的 error boundary component:
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/error-handling )
以上圖為例,當 app/dashboard/page.js
渲染發生錯誤時,就會套用 app/dashboard/error.js
的 UI;app/page.js
渲染發生錯誤則會套用 app/error.js
的 UI>
但從上面那張 components tree 的圖可以發現, <Layout>
會在 <ErrorBoundary>
的父層,所以 error.tsx
沒辦法 catch 到 layout 或 template ( 切換路由會 re-render 的 layout,後面會提到 ) 的錯誤。 假如要讓 error.tsx
catch 到 layout 的錯誤,就必須把 error.tsx
移到 layout.tsx
的父層資料夾中。
比方說,假如我希望 error boundary 可以 catch 到 app/dashboard/layout.tsx
的錯誤,我就要使用app/error.tsx
。
但假要 catch root layout 的錯誤呢?這時就必須使用另個 error boundary 的檔案 - global-error.tsx
。
global-error.tsx
會在整個應用程式外層建 error boundary,可以 catch 到所有環節的錯誤。所以當 global-error.tsx
的 error boundary 被 trigger 時,它的 fallback UI 會取代 root layout。所以 global-error.tsx
的 error boundary 必須要有 <html>
和 <body>
。
/* app/global-error.tsx */
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
做個總結:
error.tsx
,針對 route 做 error boundaryerror.tsx
放到 layout.tsx
或 template.tsx
的父層資料夾global-error.tsx
這幾週的討埨主要在單一頁面的設計,像是如何使用 Client Components 和 Server Components,以及如何做 streaming 和 error boundaries,明天開始會進入下一個單元,帶大家認識 App Router 的路由設計方式。
謝謝大家的耐心閱讀,我們明天見!