dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

React 16で導入されたError Boundaryについて

React 16 Beta がリリースされました。
Error Boundary という概念が導入されたそうなので公式ブログをざっくり読んでみます。

はじめに:React 15 までの問題

React 15 まではコンポーネントで発生したエラーをうまくハンドリングしたり、そこからリカバリするためのしくみが提供されてませんでした。
結果として

  • コンポーネントのどこかで JavaScript エラーが発生する
  • catch していない場合、知らないうちに React の内部状態(internal state)にも悪影響を及ぼす
  • 結果として開発者コンソール上はぱっと見原因がよくわからないエラーが発生する

みたいなことが起きてました。

(参考)
TypeError: Cannot read property '_currentElement' of null · Issue #4026 · facebook/react
Error: performUpdateIfNecessary: Unexpected batch number ... · Issue #6895 · facebook/react
Cannot read property 'getHostNode' of null · Issue #8579 · facebook/react


Error Boundary の導入

デモ:https://codepen.io/gaearon/pen/wqvxGa

Error Boundary は コンポーネントツリーで発生したエラーをハンドリングする ための React コンポーネントです。
ブログに記載されているサンプルコードを見てみます。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

注目すべきは componentDidCatch(error, info) という新しいライフサイクルメソッドで、これが実装されたコンポーネントのことを Error Boundary と呼ぶようです。

使うときは

<ErrorBoundary>
  <AddTodo />
  <VisibleTodoList />
  <FilterLinkList />
</ErrorBoundary>

のように、エラーをハンドリングしたいコンポーネントツリーをラップするようにして通常のコンポーネントと同じように記述します。

このようにすると、<ErrorBoundary> 以下のコンポーネントツリーで JavaScript エラーが発生したときに componentDidCatch() がエラーを捕捉してくれます。
レンダリング時だけでなく、コンストラクタやライフサイクルメソッド内で発生したエラーも捕捉できます)
try/catchcatch 句と同じですね。

そのため、あるコンポーネントツリーでエラーが発生したときもクラッシュせず
ログを送信したり、エラーメッセージ用のコンポーネントに差し替えるといった対応が可能になります。

注意

Error Boundary が捕捉するのは コンポーネントツリー内で発生したエラーのみで、自コンポーネント内で発生したエラーは捕捉できません。
つまり Error Boundary コンポーネント内のライフサイクルメソッドや render() の書き方をミスってエラーが発生した場合、それは componentDidCatch() で捕捉できません。


componentDidCatch(error, info) の引数の中身

デモ を確認した限り、

  • 第一引数の error は Error オブジェクト( messagestack をプロパティとして持つ)
  • 第二引数の infocomponentStack というプロパティを持つオブジェクト

のようです。
また componentStack には

in BuggyCounter (created by App)
in ErrorBoundary (created by App)
in div (created by App)
in App

のような情報が入っています。Component Stack Trace と呼んでいるようです。(後述)


Error Boundary はコンポーネントツリーのどこに配置すべきか

Error Boundary は複数配置することも可能なので、ツリーのどこに Boundary を設けるかは要件次第です。
アプリケーションのルートに「不明なエラー(Something went wrong)」というメッセージを表示する Boundary を1個だけ置いてもいいですし、
画面の一部分が壊れても他の部分は利用可能な状態を維持したいのであれば、その境界ごとに複数の Boundary を用意する可能性も考えられます。

例として、Facebook Messenger はサイドバーや info panel、チャット部分などはそれぞれ Error Boundary でラップしてるそうです。


Uncaught Error 時の挙動の変更

個人的にこれが大きな仕様変更だなと思いました。
React 16 から、どの Error Boundary でも捕捉されないエラー(Uncaught Error)が発生した場合 コンポーネントツリー全体が unmount され、非表示になります。

先ほどのデモを fork して試してみます。
https://codepen.io/zaki-yama/pen/dzYWNM

f:id:dackdive:20170729013405g:plain

一番下に追加したカウンター(インクリメントして 5 になるとエラー)は Error Boundary でラップしていないのですが、ここで発生したエラーのせいで画面全体がクリアされているのがわかります。


React 15 のときの挙動と比較

https://codepen.io/zaki-yama/pen/RZWVpK

f:id:dackdive:20170728125201g:plain

React 15 のときはコンソールでエラーが発生しているものの、画面自体はそのまま残っていることがわかります。


なぜこのような仕様に変更されたのか

コンポーネントツリーが unmount されて画面全体が操作不能になることより、エラーによって UI や内部状態がおかしくなったことに気づかず中途半端に動作し続けることの方がまずい、との結論に達したようです。
たとえば、商品の支払い画面の UI で実際に表示されている金額と内部で保持している金額が乖離したまま決済を実行されるリスクがあるのは危険ですね。


Component Stack Traces

React 16 ではレンダリング中に発生したエラーは必ずコンソールに出力されます。(in development とあるので NODE_ENV === development の時のみ?)
ログにはエラーメッセージと JavaScript の stack trace に加え、コンポーネントの stack trace も出力されるようになります。

f:id:dackdive:20170729020849p:plain

これにより、コンポーネントツリーのどのコンポーネントでエラーが発生したのか追いやすくなります。


さらにファイル名と行番号も出力する

component stack trace にファイル名と行番号を出力することもできます。create-react-app の場合はデフォルトで有効になっています。
creacte-react-app を使わない場合、babel-plugin-transform-react-jsx-source という Babel プラグインを追加する必要があります。
また追加した際は production build 時には無効化する必要があります。


感想

これまで、アプリで発生したエラーを React ではどのように処理すべきなのかがいまいちわかっていなかったんですが、Error Boundary の導入によって基本的な実装方法がわかりました。
エラー時に外部の error reporting サービスに送信したり、UI のパーツごとに境界を設けてあるパーツが壊れても他のパーツは機能するように、といった制御はかなりやりやすくなりましたね。

Uncaught Error 時の仕様変更については、予期せぬエラーで画面が真っ白になるという状況は避けたいので、アップデート前に最低限アプリのルートには Boundary を設けておいた方が良さそうです。


React 16 Beta を今すぐ試すには

React 16 beta · Issue #10294 · facebook/react

によると

# Yarn
yarn add react@next react-dom@next

# Npm
npm install --save react@next react-dom@next

でインストールできるそうです。未確認。