標籤:

React 父組件引發子組件重渲的時候,如何保持子組件的狀態更新不受影響?

使用 React 有一段時間了,讀了幾遍文檔,自己也寫了一個簡單的前端項目,對 React 還算熟悉(也許沒有),但是前前後後想了一個月也並沒有找到這個問題的方法。

場景:點擊按鈕發送 websocket 請求,websocket 伺服器返回數據,點擊按鈕的同時觸發按鈕的禁用倒計時,倒計時以計數的形式顯示。

矛盾:

1 websocket 數據在最外層的父組件接收,作為 state(this.state.wsData),然後傳遞給各個子組件,即使計數按鈕沒有接收任何數據,也會因為最外層父組件的 state 變化導致重渲,除非 shouldComponentUpdate 返回 false。

2 計數按鈕組件的點擊函數會更改它內部的 state,用作倒數計數的值(this.state.counter)。如果設置了 shouldComponentUpdate 返回 false,它的確防止了父組件導致的重渲,但是也禁止了自己的重渲。如果不設置 shouldComponentUpdate 為 false,那麼就會受父組件導致的重渲影響,導致計數函數會運行多次。

實現:

目前我的實現方式是在計數組件的 constructor 裡面加入一個更新開關標識變數 this.shouldupdate = false,計數組件的自更新函數的首行設置 this.shouldupdate = true 打開它,狀態更新的回調函數再設置 false 關閉即 this.setState({counter: n}, () =&> this.shouldupdate = false),然後 shouldComponentUpdate() {return this.shouldupdate}。

問題:

1 這個實現不優雅……

2 這個實現的內部執行順序不明顯(因為我沒有讀過 react 的代碼,可能也讀不懂……),我不能保證或者不能完全理解執行順序是不是正確的,只能寄希望於在給 setState 設置回調之後,子組件的狀態更新的瞬間會執行這個回調……即便如此,因為 websocket 伺服器的推送是受很多條件影響的,不確定它的推送會在何時通知子組件,不知道會不會插入到回調之前,兩個過程交叉起來總有一種不確定性,儘管現在從實現效果上來看是正確的,但代碼不能根據效果做執行保證……

3 這個問題是我在 React 中遇到的最沒有辦法解決的問題。

首先,父子組件的狀態更新會互相影響,這本來就是矛盾的。把所有的 state 都提升到父組件中也無法改變這個局面,甚至讓組件關係更加複雜,因為第一在 state 提升後,想要去影響父組件的 state,子組件就必須作為父組件的一個函數組件,成為父組件的一部分,耦合的太嚴重;第二這個子組件的 state 從邏輯上來講並沒有和其他組件公用,沒有提升的理由;

其次,這種實現需求無法迴避。我分析過這個問題的罪魁禍首到底是不是非同步請求,但非同步請求是前端最基本的構成之一。

請各位精通 React 的前輩指點一下,有什麼好的實現方法,還是我的思路就是錯的?


如果就是想這個Button自己顯示倒計時,但倒計時期間又不想被外界props改變引發重新渲染的話,用上兩層Component就好了,外層Component專門負責在countDown沒有結束的時候讓shouldComponentUpdate返回false,內層的Component根據countDown的state改變驅動只有自己的渲染。

其實,如果覺得外層做得事情足夠通用,可以寫一個HoC,差不多這樣寫:

const sealHoc = (WrappedComponent) =&> {
return class extends WrappedComponent {
constructor() {
super(...arguments);
this.sealed = false;

this.seal = this.seal.bind(this);
this.unseal = this.unseal.bind(this);
}

seal() {
this.sealed = true;
}

unseal() {
this.sealed = false;
}

shouldComponentUpdate() {
return this.sealed;
}

render() {
return &
}
}
}

內層的組件使用外層傳入的onSeal和onUnseal來「封印」和「解封」shouldComponentUpdate,差不多這麼寫。

class CountDownButton extends React.Component {
constructor() {
super(...arguments);

this.state = {countDown: 0};
this.startCountDown = this.startCountDown.bind(this);
}

startCountDown() {
this.props.onSeal this.props.onSeal();
this.setState((state) =&> ({...state, countDown: 5}));
const countDown = () =&> {
if (this.state.countDown !== 0) {
this.setState(state =&> ({countDown: state.countDown -1}));
setTimeout(countDown, 1000);
} else {
this.props.onUnseal this.props.onUnseal();
}
};
setTimeout(countDown, 1000);
}

shouldComponentUpdate(nextProps, nextState) {
return this.state.countDown !== 0;
}

render() {
return (
&

React V16 錯誤處理(componentDidCatch 示例)
【譯】React 16 測試版本
React + HOC + Redux 極簡指南

TAG:React |