錯誤邊界
在過去,component 裡 JavaScript 的錯誤常常會破壞 React 的內部 state,並使它在下次 render 的時候發生 神秘的 錯誤。這些錯誤總是被應用程式的程式碼裡更早發生的錯誤所導致,但 React 並沒有提供在 component 裡優雅處理它們的方式,而且也無法從錯誤中恢復。
引入錯誤邊界
一個介面裡的某一個 JavaScript 的錯誤不應該毀了整個應用程式。為了替 React 使用者解決這個問題,React 16 引入了一個新的概念:「錯誤邊界」。
錯誤邊界是一個 React component,它捕捉了任何在它的 child component tree 裡發生的 JavaScript 的錯誤,記錄那些錯誤,然後顯示在一個 fallback 的使用介面,而非讓整個 component tree 崩壞。錯誤邊界會在 render 的時候、在生命週期函式內、以及底下一整個 component tree 裡的 constructor 內捕捉錯誤。
注意
錯誤邊界不會在以下情況捕捉錯誤:
- Event handlers (學習更多)
- 非同步的程式碼 (例如
setTimeout
或requestAnimationFrame
callback)- Server side rendering
- 在錯誤邊界裡丟出的錯誤(而不是在它底下的 children)
一個 class component 如果定義了 static getDerivedStateFromError()
或 componentDidCatch()
其中一種(或兩種都有)生命週期,它就會變成錯誤邊界。在錯誤被丟出去之後,我們使用 static getDerivedStateFromError()
來 render fallback 的 UI,以及使用 componentDidCatch()
來記錄錯誤的資訊。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) { // 更新 state 以至於下一個 render 會顯示 fallback UI return { hasError: true }; }
componentDidCatch(error, errorInfo) { // 你也可以把錯誤記錄到一個錯誤回報系統服務 logErrorToMyService(error, errorInfo); }
render() {
if (this.state.hasError) { // 你可以 render 任何客製化的 fallback UI return <h1>Something went wrong.</h1>; }
return this.props.children;
}
}
然後你就可以把它當成一般的 component 來使用:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
錯誤邊界就如同 JavaScript 的 catch {}
,但它是給 component 使用的。只有 class component 可以成為錯誤邊界。實務上,大部分的時間你只會想要宣告錯誤邊界 component 一次,然後在你的應用程式裡重複使用它。
要注意錯誤邊界只會捕捉它底下 component tree 裡的 component 的錯誤。錯誤邊界無法捕捉它自己本身的錯誤。如果一個錯誤邊界在 render 錯誤訊息的時候失敗了,這個錯誤會被傳遞到在它之上最近的錯誤邊界。這個也與 JavaScript 的 catch {}
的運作方式類似。
Live Demo
該把錯誤邊界放在哪裡
錯誤邊界的精確度取決於你自己。你可以把它包在最上層的 route component 藉以顯示「發生了一些錯誤」的訊息給使用者,就如同 server-side framework 裡常常處理錯誤的方式。你也可以把它包在個別的小工具外,藉以保護它們不受應用程式裡發生的其他錯誤的影響。
對於未捕捉到的錯誤的新行為
這個改變有重要的意義。在 React 16,沒有被錯誤邊界所捕捉到的錯誤會 unmount 整個 React component tree。
我們為了這個決定辯論過,但在我們的經驗裡,留下壞掉的 UI 比完全移除它更糟。舉例來說,在像 Messenger 一樣的產品裡,留下壞掉的 UI 可能會導致某人傳送訊息給錯誤的對象。相似地,在支付軟體裡,顯示錯誤的金額比 render 空白畫面來得更糟。
這個改變代表著,如果你遷移到 React 16,你有可能會發掘出應用程式裡以前沒注意過但已經存在的錯誤。加上錯誤邊界使你在錯誤發生時能夠提供更好的使用者體驗。
例如,Facebook Messenger 用分開的錯誤邊界包住了側欄位的內容、資訊面板、對話紀錄、和訊息輸入欄。如果某個在其中一個 UI 裡的 component 壞了,其他的部分仍會保持能夠互動的狀態。
我們也鼓勵你使用 JS 的錯誤回報服務(或建立一個你自己的服務),這樣你可以從上線的程式裡學習未處理的 exception 並修理它們。
Component Stack Traces
React 16 把所有發生在 render 時的錯誤在開發時印出在 console 裡,即使應用程式不小心吞掉了這些錯誤。除了錯誤訊息和 JavaScript 的 stack 以外,它也提供了 component stack trace。現在你可以看到錯誤在哪個 component 裡發生的確切位置:
你也可以在 component stack trace 裡看見檔案名稱和行數。這個在 Create React App 裡是預設行為:
如果你沒有使用 Create React App, 你可以手動在 Babel 設定加上這個 plugin。注意它是被設計用來在開發模式使用的,且必須在正式環境被關掉。
注意
在 stack trace 裡顯示的 component 名稱是由
Function.name
attribute 所決定的。如果你支援沒有原生提供它的舊瀏覽器和裝置(例如 IE 11),試著考慮把Function.name
polyfill 到你的應用程式,例如function.name-polyfill
。或著,你可以另外在你所有的 component 裡設定displayName
attribute。
那 try/catch 呢?
try
/ catch
很棒,但它只作用在命令式程式碼(imperative code):
try {
showButton();
} catch (error) {
// ...
}
然而,React component 是宣告式(declarative)的,且指明了什麼必須被 render:
<Button />
錯誤邊界保有了 React 宣告式的天性,且如你所預期的運行。例如,即使在某個 tree 裡很深的地方被 setState
所導致的 componentDidUpdate
的錯誤,它仍然會正確的被傳遞到最近的錯誤邊界。
那 Event Handler 呢?
錯誤邊界不會捕捉 event handler 裡所發生的錯誤。
React 不需要從 event handler 裡發生的錯誤恢復。不像 render 和其他生命週期的函式一樣,event handler 不會發生在 render 的時候。所以如果它們丟出錯誤,React 仍然知道該顯示什麼在畫面上。
如果你需要捕捉一個 event handler 裡的錯誤,只要使用一般 JavaScript 的 try
/ catch
就可以了:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
try { // 做某些可以拋出錯誤的事情 } catch (error) { this.setState({ error }); } }
render() {
if (this.state.error) { return <h1>Caught an error.</h1> } return <button onClick={this.handleClick}>Click Me</button> }
}
注意以上的範例是用來示範一般 JavaScript 的行為,並沒有用到錯誤邊界。
從 React 15 發生的名稱改變
React 15 用不同的函式名稱支援了非常有限的錯誤邊界功能:unstable_handleError
。這個函式不能用了,而且從 16 beta 開始,你需要將它改成 componentDidCatch
。
我們為這個改變提供了 codemod 來自動遷移你的程式碼。