最近在作項目的時候遇到一個問題,在 react 組件 unmounted 以後 setState 會報錯。咱們先來看個例子, 重現一下問題:html
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 會報出以下錯誤:react
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component.jquery
這種錯誤狀況通常出如今 react 組件已經從 DOM 中移除。咱們在 react 組件中發送一些異步請求的時候, 就有可能會出現這樣的問題。舉個例子,咱們在 componentWillMount 中發送異步請求,當請求成功返回數據,咱們調用 setState 改變組件的狀態。可是當請求到達以前, 咱們更換了頁面或者移除了組件,就會報這個錯誤。這是由於雖然組件已經被移除,可是請求還在執行, 因此會報setState() on an unmounted component
的錯誤。git
好了, 咱們如今知道問題出現的緣由, 咱們該怎麼解決這個問題?思路也很簡單, 咱們只要在 react 組件被移除以前終止 setState 操做就好了。回到以前的例子, 咱們能夠這樣作:es6
componentWillMount() {
// 咱們把 setTimeout 保存在 timer 裏
this.timer = setTimeout(() => {
this.setState({
name: 'Victor Wang'
})
}, 1000)
}
// 在組件將要被移除的時候,清除 timer
componentWillUnmount() {
clearTimeout(this.timer)
}複製代碼
相似的在處理 ajax 請求的時候也是這個套路, 在 componentWillUnmount 方法中終止 ajax 請求便可,以 jquery 爲例:github
componentWillMount() {
this.xhr = $.ajax({
// 請求的細節
})
}
componentWillUnmount() {
this.xhr.abort()
}複製代碼
在處理 fetch 請求的時候思路也是同樣的, 可是處理起來就沒有那麼容易了。 由於 Promise 不能被取消, 至少從目前的規範來看是沒有相應的 API 來取消 Promise chain 的。未來可能會實現相應的 API, 感興趣的能夠看看這裏和這裏的討論。web
爲了讓 Promise 能夠被取消,咱們處理的思路是這樣的,咱們在咱們的 Promise 外面再包裹一層 Promise 來保證咱們的 Promise 能夠被取消。下面看代碼:ajax
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 請求了。promise
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 來處理這個問題。app
componentWillMount () {
// add event listeners (Flux Store, WebSocket, document, etc.)
}
componentWillUnmount () {
// remove event listeners (Flux Store, WebSocket, document, etc.)
}複製代碼
注:有什麼不對的地方, 歡迎指正!