前言
前一篇 Code Spliting 文章中有提到用 Error boundaries 來處理載入錯誤的顯示,實際上該如何實作呢?本篇會實際動手嘗試,給各位參考。
Error boundaries 是什麼?
一句話來說:Error boundaries 可以捕捉 React 元件內發生的 JavaScript 錯誤,並展示出錯誤訊息。
Error boundaries 解決什麼問題?
React 網站在運行時,可能會因為元件內部的 JavaScript 錯誤,干擾原本的 state 甚至導致下一次的 render 含有隱藏的錯誤(emit cryptic errors),這些眼前的錯誤可能來自於更早發生的錯誤,讓人頭大。React (以往)並沒有提供一個方法去處理這種情境,導致網站會無法恢復原狀。
一個 UI 上的錯誤,不應該讓整個網站受到影響,因此 React 16 推出了 Error boundaries 這個新概念。Error boundaries 可以在 rendering 的過程中(在生命週期、constructors 內)「抓住」JavaScript 錯誤,並且印出錯誤訊息、也可以自行設定 fallback 的 UI。
不適用 Error boundaries 的情境
Error boundaries 不會抓住以下的錯誤:
以及,因為 error boundary 必須使用 class component,所以無法用 hook 來寫。
實作 Error boundaries
畫面上、下半部各有一些按鈕,點擊下半部的按鈕之後,刻意製造一個錯誤,讓下半部頁面顯示錯誤訊息,但上半部不會被影響。
用 throw statement 來丟出一個自定義的意外情境,正在執行的函式會停止,而寫在 throw 之後的陳述也不會觸發。
throw new Error('Required');
// 這裡會製造出一個帶有錯誤訊息的 error 物件,稍後可以使用 Error boundaries 印出訊息
錯誤訊息可以使用 <details>
元素來展示,<details>
會建立出一個可以收合的訊息元件。
<details>
<summary>標題</summary>
訊息展開之後的內文
</details>
用 componentDidCatch() 來印出錯誤訊息,會接收兩個參數 error & info
error
- 代表程式出錯的地方丟回來的 errorinfo
- 可以取得做 Component Stack Traces 相關資訊的物件,可以用來追到實際上到底是哪一個物件出錯
實作
// 建立一個一定會壞掉的按鈕
class BreakButton extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
this.handleClick = this.handleClick.bind(this);
}
handleClick = () => {
this.setState({
hasError: true
})
}
render() {
if (this.state.hasError) {
throw new Error('這是錯誤訊息');
}
return <button onClick={this.handleClick}>Click</button>;
}
}
// 畫面結構上下各有一顆按鈕
const ErrorExample = () => {
return (
<div>
<p>這個按鈕會跳出警告</p>
<button onClick={() => { alert('這是警告') }}>
Show Alert Button
</button>
<hr />
<p>這個按鈕會壞掉</p>
<BreakButton />
</div>
);
}
在加入 error boundary 之前,看起來是這樣(以下使用 codePen):
按下壞掉的按鈕之後,整個頁面都會被錯誤卡住。
讓我們來加上 error boundary:
class component 只要包含 static getDerivedStateFromError() 或 componentDidCatch() 任一種方法就會變成 error boundary,以下會使用 componentDidCatch()。
// 注意:要使用 class component,hook 沒有 error boundary
class CustomErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, info: null };
}
// 用 componentDidCatch 來抓住錯誤
componentDidCatch(error, info) {
this.setState({
error: error,
info: info
})
}
render() {
if (this.state.error) {
return (
<>
<h1>Oops!</h1>
<details>
{this.state.error && this.state.error.toString()}
<br />
{this.state.info.componentStack}
</details>
</>
)
}
return this.props.children;
}
}
並且在原本的畫面上,加入 Error Boundary 元件,Error Boundary 會抓住子元件以下的 component tree 裡頭,所發生的錯誤。
const ErrorExample = () => {
return (
<div>
// 這裏上下半部各自用 Error Boundary 包起來
// 上半部的運作就不會被下半部的錯誤影響
<CustomErrorBoundary>
<p>這個按鈕會跳出警告</p>
<button onClick={() => { alert('這是警告') }}>
Show Alert Button
</button>
</CustomErrorBoundary>
<hr />
<CustomErrorBoundary>
<p>這個按鈕會壞掉</p>
<BreakButton />
</CustomErrorBoundary>
</div>
);
}
製作完成!以下是加上 Error Boundary 之後的效果:
可以看到錯誤訊息裡,會標注實際產生錯誤的元件名稱,並且,在下半部畫面錯誤而無法繼續運作的時候,畫面上半部的按鈕不會被影響。
===
官方文件
https://reactjs.org/docs/error-boundaries.html
Hook 不支援
API reference
https://reactjs.org/docs/react-component.html#componentdidcatch
Details tag
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
throw new error
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw
Sample by Dan
https://codepen.io/gaearon/pen/wqvxGa?editors=0010