為什麼我的 React Component 畫面不會更新 ?
為什麼我的 Component 是 Class Component 更新行為正常,改為 Pure Component 或是 Functional Component 搭上 useState 的 Hooks 就不會動了呢?
我們先寫一個 Class Component
可以正常更新畫面,沒有問題,你也可以在底下的 Codepen 試試看
接下來,我們繼續改寫成 PureComponent
只要簡單的把 class App extends React.Component
改為 class App extends React.PureComponent
就可以了!
你一樣可以利用底下的 Codepen 試試!
你會發現,一模一樣的程式碼居然不會動了?為什麼?
而且你會發現,當你點下按鈕的時候,其實 console.log
是有作用的,在瀏覽器的開發者工具 (devTool) 中,其實拿到的資料會更新,畫面卻不會更新,究竟是為什麼呢?
Component 與 PureComponent 的差異
React Component
在 React 的世界當中,Component 不管是 props 或是 state 的改變
一律重繪 (重新 render 畫面)
但在 PureComponent 呢?
讓我們來看看官方說了什麼 — React.PureComponent
React.PureComponent
is similar toReact.Component
. The difference between them is thatReact.Component
doesn’t implementshouldComponentUpdate()
, butReact.PureComponent
implements it with a shallow prop and state comparison.
也就是說 PureComponent 實作了 React Component 生命週期中的 shouldComponentUpdate()
,他會「淺層比較 Component 的前一次與後一次 的 props 與 state,來決定要不要更新」。
什麼是淺層比較 (Shallow Equal)?
簡單的來說,其實就 JavaScript 中的 ===
運算子(comparison operator),如果你常寫 JavaScript 應該會很常用到它!也就是說,如果你有兩個 JavaScript 物件的變數 a 跟 b 要做淺層比較,就會使用 a === b
。
適用於 JavaScript 的物件型別 (包含物件與陣列),JavaScript 的六種基本型別不在討論範圍內 (包含 Boolean, undefined, null, String, Number 與 Symbol)。
如果你還不知道什麼是
===
,請看 MDN — Comparison Operator
如果你理解了什麼是淺層比較
那麼 PureComponent 其實就是做了以下的比較
shouldComponentUpdate(nextProps, nextState) {
return !(this.props === nextProps) || !(this.state === nextState)
}
注意:真正的原始碼其實是使用
fbjs
library 中的shallowEqual
方法,含有更多的例外情況處理,為顧及各個程度的人,這邊只是簡單描述。
這樣一來你知道為什麼上面的範例 PureComponent 不會更新了嗎?
我在 handleClick
的 console.log
,增加印出 this.state.data === newData
,你就會發現,不管狀態中的陣列怎麼被增加元素,他的比較結果永遠都會是「一樣的」。
JavaScript 的物件也有一樣的問題
狀態中原本有一個 name 的屬性,按下按鈕要增加 describe
的屬性,結果會跟陣列是一樣的,原因也相同,就是因為 this.state.data === newData
是 true。
那我們要怎麼樣避免這個問題呢?
其實當你需要更新狀態的時候
每次物件操作都複製一份「新」的資料出來
操作後再丟給 setState,就一定會更新了!
陣列 Array 的複製新資料方法
// ES5 版本
var newData = this.state.data.slice()
newData === this.state.data // 回傳 false// ES6 版本
let newData = [ ...this.state.data ]
newData === this.state.data // 回傳 false
物件 Object 的複製新資料方法
// ES5 版本
var newObject = Object.assign({}, this.state.data)
newObject === this.state.data // 回傳 false// ES6 版本
let newObject = { ...this.state.data }
newObject === this.state.data // 回傳 false
實際的例子
結語
這個是新手在學習 React 的時候常常會遇到的問題,也很多人不知道怎麼解釋這個問題,不知道這樣一連串的講解下來,這樣一來是不是可以讓你完整了解整個 React Component 的更新機制了呢?
同場加映
前陣子 React Hooks 剛釋出,燒燙燙也很夯,也很多人迫不及待的要使用新的 Hooks API,假設你都知道 Hooks API 怎麼使用,那你也一定知道 Functional Component 如何實作 PureComponent 的方法 memo
。
如果你還不知道
memo
是什麼,請看這裡
但是使用 React Hooks useState API 實作的 Functional Component 有個跟 React Component 不同的地方,讓我們直接看範例吧,我們實作跟一開始的 React Component 一樣的方法,但不使用 memo
API,所以我們「預期」行為應該要跟 React Component 一樣,遵守「一律重繪」的原則,but …
人生最怕就是這個 But
什麼!這個 Functional Component 沒實作 memo
API 居然不能跟 React Component 一樣正常更新?不是應該要跟 React Component 有一樣的行為嗎?
為什麼?
因為 Hooks 的 useState 實作
抽絲剝繭,跑去看了 React 原始碼,原因其實出在 useState
的實作,最後會走向 shallowEqual 的方法,「判斷之前的狀態與之後的狀態」來決定是不是要更新 Component,所以其實 useState 自帶 shallowEqual 的比較,請務必特別注意。
- useState 的 shallowEqual 原始碼:https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js#L652
Hi 我是 Ryan,如果這篇文章有幫助到你,請你不吝嗇的給予我鼓掌,那將是我進步的動力!👏
另外,你知道 Medium 一篇文章拍手其實可以拍 50 下嗎?如果你願意,請賜我掌聲,謝謝!