React 基礎:派生 state 的“錯誤使用”示例



前言

近年前端技術層出不窮,Facebook 推出的 React 也是程序員間討論最多的 Javascript 框架之一。借這個機會開始學習 React,並把筆記作為 React 基礎教程系列(參考 React 官網)。

因為屬於入門,不太適合對 React 有經驗的同學,但希望到此的同學多少都有所收穫。

為了更好的閱讀體驗,可以在文章底下的“瞭解更多”中查看原文

派生 state

我語文可能是體育老師教的,對於派生這個詞語不知道是什麼意思,先解釋下什麼是"派生":

派生(derived):本指江河的源頭產生出支流。引申為從一個主要事物的發展中分化出來。

那麼放在 Javascript 中,對於"派生 state"就好理解了,無非就是 state 狀態對象的值由其他變量賦值得到,而不是我們對 state 進行初始化創建的。


那麼就對於這樣的 state ,在如下兩個場景就會有"副作用",使得程序出現非預期的 bug 問題。


避免將 props 的值複製給 state

場景說明:

父組件 EmailReset 中有個定時器,頁面渲染後將觸發定時器,不斷刷新 state 狀態對象;

子組件 EmailInput 有個 Input 輸入框,接受 props.email 屬性,並將其作為 Input 輸入框的 value 顯示。


代碼示例:

頁面 HTML 展示:


父組件 EmailReset:


注意:

在 componentDidMount 生命週期中,定義並執行定時器 timer;

並對子組件 EmailInput 設置屬性 email(此 email 和 state 並沒有關係,此處還是個"死值")。


子組件 EmailInput:


一個典型 React 受控組件例子,Input 輸出後觸發事件方法,更新 state 對象重新渲染後回顯到頁面上。


問題:

看似沒有問題,但當用戶對 Input 輸入數據後,會被初始化成原始值。


頁面 HTML 效果:


結合瀏覽器控制檯調試信息,和頁面 HTML Input 輸入框所示,我們看到三種現象:

1. 每次父組件定時器執行,更新 state 對象,觸發 render 方法

2. 之後,每次還會觸發子組件的生命週期方法:static getDerivedStateFromProps componentWillReceiveProps(現在不建議使用)

3. 頁面 HTML 的 Input 輸入框值被還原


那怎麼解決?

代碼示例:


嘗試更改邏輯設計,增加如上判斷。這樣每次父組件 state 狀態的更新,雖然還會觸發子組件的生命週期 componentWillReceiveProps 方法,但由於這樣的判斷,就不會重置 email 數據狀態。


好像展示解決的這個 bug,但稍有疏忽還會引發其他問題。


在 props 變化後修改 state

場景說明:

針對上面那個解決方案,再引申舉個反例。


比如,一個頁面 HTML 中有多個 tab 標籤,每個 tab 對應不同的 Input 輸入框初始數據。


代碼示例:

父組件 EmailFormTab:


定義父組件的 tab 欄,以及點擊事件 onChooseTab:


邏輯這樣:點擊不同 tab,觸發對應事件,獲取點擊 tab 索引,更新對應 state 狀態;從而每次渲染後,currentEmail 為 emailList 對應的 email 值。


子組件同上。


問題:

看似也沒什麼問題,但注意前兩個 tab 對應的 email 數據相同時,每當在 tab1 下,對 input 輸入新值,再點擊 tab2 ,會使得 input 變為前面輸入的值。

這肯定不是我們設計上的原意(雖然公用一個 input,邏輯上每次切換 tab 時,希望他們保持獨立)。


頁面 HTML 效果:


原因就是 static getDerivedStateFromProps or componentWillReceiveProp 根本就是有問題的。



注意,props.email (每次點擊 tab 時,傳入的值) 和 this.props.email (初始化時的值)的判斷永遠進入不了,導致 state 一直更改不了。

input 的 value 一直沿用之前 currentEmail.email (tab1 輸入時的值)。


解決方案

使用"完全"可控的組件

由於子組件 EmailInput 對於 Input 上事件觸發,都是通過 state 狀態自身管理。所以需要將 onChange 移出子組件,以及 value 對應的 state 取值,都放到父組件中控制,再通過數據屬性 props 傳遞給子組件。

這樣子組件就是個完全可控的組件了。

子組件,將 state 替換為 props:

父組件:

重新定義初始化的 state 狀態對象:

當父組件觸發響應方法後,更新 state :

通過屬性定義,傳入子組件:

頁面 HTML 效果:


使用有 key 描述的可控組件

把代碼恢復到之前的樣子,只添加 key 屬性,讓每次渲染子組件時,都根據 key 新創建獨立的子組件。

代碼示例:


總結

根據兩個不恰當的例子說明,我們基本瞭解對派生 state 的使用要及其注意。

static getDerivedStateFromProps componentWillReceiveProps 兩個生命週期中添加的邏輯要慎重!因為說不定會像上例中出現"重置的效果",也不要試圖用自認為沒問題的邏輯判斷解決派生 state 相關問題。

另外區分好受控和非受控組件的含義。有必要時,不要怕麻煩,在父組件中定義好事件函數傳給子組件;

也可以定義 key 屬性,或者人為邏輯中增加獨一的 ID 設計以區分。