React・仕事で使うために

画面全体を落とさない(Error BoundaryとSuspense)

Reactは1か所のエラーで画面がまるごと消えます。それを部分的に受け止めるのがError Boundaryです。クラスコンポーネントを書く唯一の場面、置く場所の決め方、そしてlazyとSuspenseによる分割読みこみまで扱います。

debuggingの回で、エラーの読み方を扱いました。今回はエラーが起きたときに、画面をどう壊れさせるかの設計です。

何もしないと、画面がまるごと消える

Reactは、描画中にエラーが起きるとその画面全体を空にします。1つのカードのバグで、ページ全部が真っ白になるということです。

開発中は赤いエラー画面が出るので気づきますが、本番では何も出ません。ユーザーから見えるのは白紙です。

Error Boundaryはクラスで書く

Reactでいまクラスコンポーネントを書く唯一の場面がこれです。描画中のエラーを捕まえる仕組みが、フックとして用意されていないためです。

import { Component } from 'react'

class ErrorBoundary extends Component {
  state = { hasError: false }

  static getDerivedStateFromError() {
    return { hasError: true }   // 落ちたことを状態に記録する
  }

  componentDidCatch(error, info) {
    // 本番ではここでエラー監視サービスへ送る
    console.error('Caught by ErrorBoundary:', error, info.componentStack)
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback ?? (
          <div className="p-8">
            <h1 className="text-2xl font-bold">画面の表示に失敗しました</h1>
            <button type="button" onClick={() => this.setState({ hasError: false })}>
              もう一度ためす
            </button>
          </div>
        )
      )
    }

    return this.props.children
  }
}

export default ErrorBoundary

2つのメソッドが仕事をしています。

  • getDerivedStateFromError——落ちたことをstateに記録する(表示の切りかえ用)
  • componentDidCatch——記録を送るための場所(監視サービス・ログ)

使うときは、守りたい範囲を包みます

<ErrorBoundary>
  <Routes>…</Routes>
</ErrorBoundary>
「画面の表示に失敗しました」という見出しと「もう一度ためす」ボタンだけが表示されている
わざと落としたときの画面。真っ白ではなく、戻る手段が出ているのが目的です

どこに置くか

アプリ全体に1つ+壊れても他を生かしたい単位、という二段構えが基本です。

置き場所効果
アプリ全体(<Routes>の外)最後の砦。真っ白を防ぐ
ページ単位そのページだけ落ちる。ヘッダーやナビは生きる
部品単位(一覧・グラフ・広告枠)部品だけ落ちて、周りは表示されたまま

捕まえられないエラーがある

ここは誤解しやすいところです。Error Boundaryが捕まえるのは描画中のエラーだけです。

捕まる捕まらない
描画中(JSXを組み立てている最中)イベントハンドラの中onClickの処理)
ライフサイクル・useEffectの中の描画関連非同期処理の中setTimeoutfetch.then
子コンポーネントの描画サーバー側の描画(別の仕組みが要る)

イベントの中はtry/catch、データ取得はライブラリのエラー状態useQueryisError)で扱います。

function handleDelete() {
  try {
    removeBird(bird.id)
  } catch (error) {
    setMessage('削除できませんでした')
  }
}

lazySuspense——読みこみを分ける

Error Boundaryと同じ「範囲を区切る」考え方で、読みこみにも区切りを入れられます。

import { lazy, Suspense } from 'react'

const BirdDetailPage = lazy(() => import('./pages/BirdDetailPage.jsx'))

<Suspense fallback={<p>よみこみ中…</p>}>
  <BirdDetailPage />
</Suspense>

lazyにすると、そのページを開いたときに初めてJavaScriptが読みこまれます(コード分割)。最初に配るファイルが小さくなるので、初回表示が速くなります。

Suspenseその待ち時間に何を出すかの指定です。

公式ドキュメントも見てみる

ja.react.dev外部サイトComponent – React公式のError Boundaryの説明。getDerivedStateFromErrorcomponentDidCatchの役割分担が書かれています

このレッスンのまとめ

  1. Reactは描画中のエラーで画面をまるごと空にする(本番では真っ白)
  2. Error Boundaryで、壊れる範囲を自分で決める
  3. いまクラスコンポーネントを書く唯一の場面。ライブラリで代替もできる
  4. getDerivedStateFromErrorで表示を切りかえ、componentDidCatchで記録を送る
  5. 置き方はアプリ全体+落ちても他を生かしたい単位の二段構え
  6. イベントハンドラと非同期処理の中は捕まらない——try/catchやライブラリのエラー状態で
  7. lazySuspenseで読みこみも区切れる。ErrorBoundaryと重ねると3状態が表せる

次からは章が変わって、TypeScriptです。ここまで書いたJavaScriptに型を入れていきます。

やってみよう:Error Boundaryを入れて、わざと落とす

落ちる画面を作って、受け止められることを確認します。

  1. src/components/ErrorBoundary.jsxを上のコードで作る
  2. App.jsx<Routes><ErrorBoundary>で包む
  3. わざと落ちるページを作る(下のコード)
  4. /boomを開いて、真っ白ではなく代替表示が出ることを確認する
  5. 「もう一度ためす」を押して、状態が戻ることを確認する
  6. 確認できたら、BoomPageとルートを消す
function BoomPage() {
  const bird = null
  return <p>{bird.name}</p>   // null.name で落ちる
}

export default BoomPage
「画面の表示に失敗しました」と「もう一度ためす」ボタンが表示されている
これが出れば成功。ユーザーが行き止まりにならないのが大事な点です

余力があれば、onClickの中でthrow new Error('test')と書いて、Error Boundaryが反応しないことも確かめてください。捕まる範囲を体で覚えられます。

よくある質問

Reactでエラーが起きると何が起きますか?
受け止める仕組みが無いと、Reactはその画面をまるごと空にします。開発中は赤いエラー画面が出ますが、本番では真っ白なページになります。
Error Boundaryはなぜクラスコンポーネントなのですか?
描画中のエラーを捕まえる仕組みが、関数コンポーネントのフックとして提供されていないためです。自分で書く場合はクラスになりますが、react-error-boundaryのようなライブラリを使えば書かずに済みます。
どこに置けばいいですか?
アプリ全体に1つ置いたうえで、壊れても他が生きていてほしい単位にも置きます。一覧とサイドバーを別々に包めば、片方が落ちてももう片方は表示されます。
Error Boundaryはすべてのエラーを捕まえますか?
いいえ。描画中のエラーだけです。イベントハンドラの中や非同期処理の中で投げられたエラーは捕まりません。そこはtry/catchや、データ取得ライブラリのエラー状態で扱います。