React 16のUncaught Errorに対策して、ErrorBoudaryおよびErrorBoudaryHOC(HigherOrderComponent)を作成する
Uncaught Error
時のReactの挙動
React 15: エラーが発生しても、UIが変わらない
React 16 以降: コンポーネントツリーが全部unmount
される
As of React 16, errors that were not caught by any error boundary will result in unmounting of the whole React component tree.
React 16のErrorBoudary
の対応
ErrorBoudaryを作成して、componentDidCatch
を使う
ErrorBoudary
- 子孫コンポーネントの中にエラーが発生したら、
catch
できる -
componentDidCatch
というfunctionを使って、発生したエラーをハンドリングできる - ErrorBoudaryはいくつでも利用できる
componentDidCatch(error, errorInfo)
- JavaScript
catch
みたい - error: react error
- errorInfo: コンポーネントのエラーが発生したところがわかりやすい
The above error occurred in the <TodoItem> component:
in TodoItem (created by TodoApp)
in div (created by TodoList)
in TodoList (created by TodoApp)
in ErrorBoundary (created by TodoApp)
in div (created by TodoApp)
in TodoApp
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
Code Demo jsfiddle
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {error: null, errorInfo: null};
}
componentDidCatch(error, errorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({error, errorInfo})
// You can also log error messages to an error reporting service here
}
render() {
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{whiteSpace: 'pre-wrap'}}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
// Normally, just render children
return this.props.children;
}
}
ErrorBoundaryを使用する
<ErrorBoundary>
<TodoList/>
<AddTodo/>
</ErrorBoundary>
エラーハンドリングのカスタマイズ jsfiddle
PropとしてonError
(発生したエラーをハンドリングするfuntion)とErrorComponent
(Error message)を渡します。
const ErrorComponent = ({error, errorInfo}) => (
<div>
<h2>Something went wrong.</h2>
<details style={{whiteSpace: 'pre-wrap'}}>
{error && error.toString()}
<br />
{errorInfo.componentStack}
</details>
</div>
)
const onError = (error, errorInfo) => (
console.error(error, errorInfo)
)
<ErrorBoundary ErrorComponent={ErrorComponent} onError={onError}>
...
</ErrorBoundary>
Higher Order Component としてErrorBoudaryを利用する
コンポネートごとにErrorBoudaryを使うと、ある時が面倒です。
<ErrorBoundary>
<TodoList>
<ErrorBoundary>
<TodoItem/>
</ErrorBoundary>
<ErrorBoundary>
<TodoItem/>
</ErrorBoundary>
</TodoList>
<AddTodo/>
</ErrorBoundary>
-> Higher Order Componentを利用する
class ErrorBoundary extends React.Component {
...
}
function ErrorBoundaryHOC(Component){
return class ErrorBoundaryComponent extends React.Component {
render(){
return (
<ErrorBoundary>
<Component {...this.props} />
</ErrorBoundary>
)
}
}
}
上記の問題は解決できます。また、HOCを使うと、view側は何も変更しないで、ErrorBoundaryも適用できる。
const TodoItemHOC = ErrorBoundaryHOC(TodoItem)
//Viewを変更したくない
//const TodoItem = ErrorBoundaryHOC(require('./TodoItem')
<ErrorBoundary>
<TodoList>
<TodoItemHOC/>
<TodoItemHOC/>
</TodoList>
<AddTodo/>
</ErrorBoundary>
##お負け
React 15みたいにUIが変わりたくない
前述によると、エラーが発生したら、ErrorComponent
をレンダリングですが、多分時々そこをやりたくないと思います。Userにとってもエラーメッセージを見たくないな。
そこを実装するために、単純にエラーが発生する時、ErrorComponent
をレンダリングしないで、childrenをレンダリングします。
render() {
// Normally, just render children
return this.props.children;
}
エラーハンドリングがデフォルトにして、全プロジェクトを使う
ErrorBoundaryを作成する
ErrorBoundary.defaults = {
ErrorComponent: null,
onError: null
}
ErrorBoundary.setOptions = function(opt) {
merge(ErrorBoundary.defaults, opt)
}
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
const {onError, ErrorComponent} = this.props
//propsが優先される
const opt = merge({}, ErrorBoundary.defaults, {onError, ErrorComponent}}
this.state = merge({error: null, errorInfo: null}, opt}
}
}
ErrorBoundaryを利用する
import ErrorBoundary from './ErrorBoundary'
ErrorBoundary.setOptions({onError, ErrorComponent})