在React組件unmounted之后setState的報錯處理


最近在做項目的時候遇到一個問題,在 react 組件 unmounted 之后 setState 會報錯。我們先來看個例子, 重現一下問題:

class Welcome extends Component { state = { name: '' } componentWillMount() { setTimeout(() => { this.setState({ name: 'Victor Wang' }) }, 1000) } render() { return <span>Welcome! {this.state.name}</span> } } class WelcomeWrapper extends Component { state = { isShowed: true } componentWillMount() { setTimeout(()=> { this.setState({ isShowed: false }) }, 300) } render() { const message = this.state.isShowed ? <Welcome /> : 'Bye!' return ( <div> <span>{ message }</span> </div> ) } }

舉的例子不是很好,主要是為了說明問題。在 WelcomeWrapper 組件中, 300ms 之后移除了 Welcome 組件,但在 Welcome 組件里 1000ms 之后會改變 Welcome 組件的狀態。這時候 React 會報出如下錯誤:

Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component.

這種錯誤情況一般出現在 react 組件已經從 DOM 中移除。我們在 react 組件中發送一些異步請求的時候, 就有可能會出現這樣的問題。舉個例子,我們在 componentWillMount 中發送異步請求,當請求成功返回數據,我們調用 setState 改變組件的狀態。但是當請求到達之前, 我們更換了頁面或者移除了組件,就會報這個錯誤。這是因為雖然組件已經被移除,但是請求還在執行, 所以會報setState() on an unmounted component的錯誤。

決解問題

好了, 我們現在知道問題出現的原因, 我們該怎么解決這個問題?思路也很簡單, 我們只要在 react 組件被移除之前終止 setState 操作就行了。回到之前的例子, 我們可以這樣做:

componentWillMount() {
  // 我們把 setTimeout 保存在 timer 里 this.timer = setTimeout(() => { this.setState({ name: 'Victor Wang' }) }, 1000) } // 在組件將要被移除的時候,清除 timer componentWillUnmount() { clearTimeout(this.timer) }

類似的在處理 ajax 請求的時候也是這個套路, 在 componentWillUnmount 方法中終止 ajax 請求即可,以 jquery 為例:

componentWillMount() {
  this.xhr = $.ajax({ // 請求的細節 }) } componentWillUnmount() { this.xhr.abort() }

在處理 fetch 請求的時候思路也是一樣的, 但是處理起來就沒有那么容易了。 因為 Promise 不能被取消, 至少從目前的規范來看是沒有相應的 API 來取消 Promise chain 的。將來可能會實現相應的 API, 感興趣的可以看看這里這里的討論。

fecth 請求的處理

為了讓 Promise 可以被取消,我們處理的思路是這樣的,我們在我們的 Promise 外面再包裹一層 Promise 來保證我們的 Promise 可以被取消。下面看代碼:

const makeCancelable = (promise) => { let hasCanceled_ = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then((val) => hasCanceled_ ? reject({isCanceled: true}) : resolve(val) ); promise.catch((error) => hasCanceled_ ? reject({isCanceled: true}) : reject(error) ); }); return { promise: wrappedPromise, cancel() { hasCanceled_ = true; }, }; };

這個 pattern 是由@istarkov提出來的。
上面用到 Promise 的相關知識, 不熟悉 Promise 的同學可以參考這里
現在我們就可以用 makeCancelable 來取消我們的 fetch 請求了。

componentWillMount() {
  // 為了簡單和方便, 這里我用 setTimeout 來模仿一個需要很長時間的 fetch 請求 const mimicFetch = (resolve, reject) => { setTimeout(() => { resolve('Victor Wang') }, 1000) } const promise = new Promise(mimicFetch) this.cancelable = makeCancelable(promise) this.cancelable.promise.then(name => { this.setState({ name }) }, (e) => { console.log(e) }) } componentWillUnmount() { // 在這取消 this.cancelable.cancel() }

預防錯誤

為了避免這種錯誤的發生,我們有一個通用的 pattern 來處理這個問題。


componentWillMount () {
// add event listeners (Flux Store, WebSocket, document, etc.) } componentWillUnmount () { // remove event listeners (Flux Store, WebSocket, document, etc.) }

注:有什么不對的地方, 歡迎指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM