iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 16

Day 16 - 如何防止整頁白頁:Error Boundaries & error.tsx

  • 分享至 

  • xImage
  •  

昨天介紹了使用 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 處理完後會發生什麼事:
no error boudaries demo

會直接白頁,因為當 render 遇到問題時,React 預設會直接移除頁面 UI。

React Error Boundary Component

所以在寫 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

那假如我想針對 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。
how error.tsx works
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/error-handling)

以上述例子來說,當 /products/page.tsx 渲染發生錯誤時,頁面中間就會顯示「發生問題,請稍後再試」,而 layout ( header 和 footer ) 依然會存在頁面,且 layout 中的 state 也不會被重置。
page error with error boundaries

假如不知道什麼是 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>
  )
}

針對 Component 的 Error Boundary

假如使用 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 Boundaries

那假如我的專案中,有很多個 error.tsx 和 error boundary components,Next 會怎麼決定 component 渲染發生錯誤時,error boundary 要套用哪個呢?

當 component 渲染發生錯誤時,Next 會往 component tree 父層找最接近 component 的 error boundary component
error component tree
( 圖片來源: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>
  )
}

做個總結:

  1. 假如想避免某個 component 渲染錯誤造成整頁白頁,可以使用 error boundary
  2. App Router 可以使用 error.tsx,針對 route 做 error boundary
  3. 一樣可以使用 React error boundary component 來針對 component 做 error boundary
  4. 想針對 layout 做 error boundary,要把 error.tsx 放到 layout.tsxtemplate.tsx的父層資料夾
  5. 想針對 root layout 做 error boundary,可以使用 global-error.tsx

這幾週的討埨主要在單一頁面的設計,像是如何使用 Client Components 和 Server Components,以及如何做 streaming 和 error boundaries,明天開始會進入下一個單元,帶大家認識 App Router 的路由設計方式。

謝謝大家的耐心閱讀,我們明天見!


上一篇
Day 15 - 提升 Server-Side Rendering 的使用者體驗:Streaming、Suspense 與 loading.tsx
下一篇
Day 17 - Next.js 13 App Router 基本路由設定
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言