摘要:同時,我們意識到人們對于這兩個鉤子函數的使用有許多誤解,也發現了一些造成這些晦澀的反模式。注意事項本文提及的所有反模式案例面向舊鉤子函數和新鉤子函數。因此,用這兩個鉤子函數來無條件消除是不安全的。
原文鏈接:https://reactjs.org/blog/2018...
React 16.4包含了一個getDerivedStateFromProps的 bug 修復:曾帶來一些 React 組件頻繁復現的 已有bug。如果你的應用曾經采用某種反模式寫法,但是在這次修復之后沒有被覆蓋到你的情況,我們對于該 bug 深感抱歉。在下文,我們會闡述一些常見的,derived state相關的反模式,還有我們的建議寫法。
很長一段時間,componentWillReceiveProps是響應props 改變,不會帶來額外重新渲染,更新 state 的唯一方式。在16.3版本中,我們引入了一個生命周期方法getDerivedStateFromProps,為的是以一種更安全的方式來解決同樣的問題。同時,我們意識到人們對于這兩個鉤子函數的使用有許多誤解,也發現了一些造成這些晦澀 bug 的反模式。getDerivedStateFromProps的16.4版本修復使得 derived state更穩定,濫用情況會減少一些。
注意事項本文提及的所有反模式案例面向舊鉤子函數componentWillReceiveProps和新鉤子函數getDerivedStateFromProps。
本文會涵蓋下面討論:
什么時候去使用 derived state
一些 derived state 的常見 bug
反模式:無條件地拷貝props 到state
反模式:當 props 改變的時候清除 state
建議解決方案
內存化
什么時候去使用Derived StategetDerivedStateFromProps存在的唯一目的是使得組件在 props 改變時能都更新好內在state。我們之前的博文有過一些例子,比如基于一個變化著的偏移 prop 來記錄當前滾動方向或者根據一個來源 prop 來加載外部數據。
我們沒有給出許多例子,因為總體原則上來講,derived state 應該用少點。我們見過的所有derived state 的問題大多數可以歸結為,要么沒有任何前提條件的從 props 更新state,要么 props,state 不匹配的任何時候去更新 state。(我們將在下面談及更多細節)
如果你正在使用 derived state 來進行一些基于當前 props 的內存化計算,那么你不需要 derived state。memoization 小節會細細道來。
如果你在無條件地更新 derived state或者 props,state 不匹配的時候去更新它,你的組件很可能太頻繁地重置 state,繼續閱讀可見分曉。
derived state 的常見 bug受控,不受控概念通常針對表單輸入,但是也可以用來描述組件的數據活動。props 傳遞進來的數據可以看成受控的(因為父組件控制了數據源)。組件內部狀態的數據可以看成不受控的(因為組件能直接改變他)。
最常見的derived state錯誤 就是混淆兩者(受控,不受控數據);當一個 state 的變更字段也可以通過 setState 調用來更新的時候,就沒有一個單一的(真相)數據源。上面談及的加載外部數據的例子可能聽起來情況類似,但是一些重要方面還是不一樣的。在加載例子中,source 屬性和 loading 狀態有著一個清晰數據源。當source prop改變的時候,loading 狀態總是被重寫。相反,loading 狀態只會在 prop 改變的時候被重寫,其他情況下就是被組件管控著。
問題就是在這些約束變化的時候出現的。最典型的兩種形式如下,我們來瞧瞧:
反模式: 無條件的從 props 拷貝至 state一個常見的誤解就是以為getDerivedStateFromProps和componentWillReceivedProps會只在props 改變的時候被調用。實際上這兩個鉤子函數可能在父組件渲染的任何時候被調用,不管 props 是不是和以前不同。因此,用這兩個鉤子函數來無條件消除 state 是不安全的。這樣做會使得 state 更新丟失。
我們看看一個范例,這是一個郵箱輸入組件,鏡像了一個 email prop 到 state:
class EmailInput extends Component { state = { email: this.props.email } render () { return } handleChange = e => { this.setState({ email: e.target.value }) } componentWillReceiveProps(nextProps) { // This will erase any local state updates! // Do not do this. this.setState({ email: nextProps.email }) } }
剛開始,該組件可能看起來 Okay。State 依靠 props 來進行值初始化,我們輸入的時候也會更新 State。但是如果父組件重新渲染的時候,我們敲入的任何字符都會被忽略。就算我們在 鉤子函數setState 之前進行了nextProps.email !== this.state.email的比較,也無濟于事。
在這個簡單例子中,我們可以通過增加shouldComponentUpdate,使得只在 email prop改變的時候重新渲染。但是實踐表明,組件通常會有多個 prop,另一個 prop的改變仍舊可能造成重新渲染還是有不正確的重置。函數和對象類型的 prop 經常行內生成。使得shouldComponentUpdate只允許在一種情形發生時返回 true很難實現。這兒有個直觀例子。所以,shouldComponentUpdate是性能優化的最佳手段,不要想著確保 derived state 的正確使用。
希望現在的你明白了為什么無條件拷貝 props 到 state 是個壞主意。在總結解決方案之前,我們來看看相關反模式:如果我們指向在 email prop 改變的時候去更新 state 呢
反模式: props 改變的時候擦除 state
接著上面例子繼續,我們可以避免在 props.email改變的時候故意擦除 state:
class EmailInput extends Component { state = { email: this.props.email } componentWillReceiveProps(nextProps) { // Any time props.email changes, update state. if (nextProps.email !== this.props.email) { this.setState({ email: nextProps.email }) } } }注意事項
即使上面的例子中只談到 componentWillReceiveProps, 但是也同樣適用于getDerivedStateFromProps。
我們已經改善許多,現在組件會只在props 改變的時候清除我們輸入過的舊字符。
但是還有一個殘留問題。想象一下一個密碼控件在使用上述輸入框組件,當涉及到擁有同一郵箱的兩個帳號的細節式,輸入框無法重置。因為 傳遞給組件的prop值,對于兩個帳號而言是一樣的。這會困擾到用戶,因為一個賬號還沒保存的變更將會影響到共享同一郵箱的其他帳號。這有demo。
這是個根本性的設計失誤,但是也很容易犯錯,比如我。幸運的是有兩個更好的方案。關鍵在于,對于任何片段數據,需要用一個多帶帶組件來保存數據,并且要避免在其他組件重復。我們來看看這兩個方案:
解決方案 推薦方案一:全受控組件避免上面問題的一個辦法,就是從組件當中完全移除 state。如果我們的郵箱地址只是作為一個 prop 存在,那么我們不用擔心和 state 的沖突。甚至可以把EmailInput轉換成一個更輕量的函數組件:
function EmailInput(props) { return }
這個辦法簡化了組件的實現,如果我們仍然想要保存草稿值的話,父表單組件將需要手動處理。這有一個這種模式的demo。
推薦方案二: 帶有 key 屬性的全不受控組件另一個方案就是我們的組件需要完全控制 draft 郵箱狀態值。這樣的話,組件仍然可以接受一個prop初始值,但是會忽略該prop 的連續變化:
class EmailInput extends Component { state = { email: this.props.defaultEmail } handleChange = e => { this.setState({ email: e.target.value }) } render () { return } }
在聚焦到另一個表單項的時候為了重置郵箱值(比如密碼控件場景),我們可以使用React 的 key 屬性。當 key 變化時,React 會創建一個新組件實例,而不是更新當前組件。Keys 通常對于動態列表很有用,不過在這里也很有用。在一個新用戶選中時,我們用 user ID 來重新創建一個表單輸入框:
每次 ID 改變的時候,EmailInput輸入框都會重新生成,它的 state 也就會重置到最新的 defaultEmail值。栗子不能少,這個方案下,沒有必要把 key 值添加到每個輸入框。在整個form表單上 添加一個 key 屬性或許會更合理。每次 key 變化時,表單內的所有組件都會重新生成,同時初始化 state。
在大多數情況,這是處理需要重置的state的最佳辦法。
注意事項這個辦法可能聽起來性能慢,但是實際表現上可能微不足道。如果一個組件有復雜更新邏輯的話使用key屬性可能會更快,因為diffing算法走了彎路
方案一:通過 ID 屬性重置 uncontrolled 組件
如果 key 由于某個原因不生效(有可能是組件初始化成本高),那么一個可用但是笨拙的辦法就是在getDerivedStateFromProps里監聽userID 的變化。
class EmailInput extends Component { state = { email: this.props.defaulEmail, pervPropsUserID: this.props.userID, } static getDerivedFromProps(nextProps, prevState) { // Any time the current user changes, // Reset any parts of state that are tied to that user. // In this simple example, that"s just the email. if (nextProps.userID !== prevState.prevPropsUserID) { return { prevPropsUserID: nextProps.userID, email: nextProps.defaultEmail, } } return null } // ... }
如果這么做的話,也給只重置組件部分內在狀態帶來了靈活性,舉個例子。
注意事項即使上面的例子中只談到 getDerivedStateFromProps, 但是也同樣適用于componentWillReceiveProps。
方案二:用實例方法來重置非受控組件
極少情況下,即使沒有用作 key 的合適 ID,你還是想重置 state。一個辦法是把 key重置成隨機值或者每次你想重置的時候會自動糾正。另一個選擇就是用一個實例方法用來命令式地重置內部狀態。
class EmailInput extends Component { state = { email: this.props.defaultEmail, } resetEmailForNewUser (newEmail) { this.setState({ email: newEmail }) } // ... }
父表單組件就可以使用一個 ref 屬性來調用這個方法,這里有 Demo.
總結總結一下,設計一個組件的時候,重要的是確定數據是受控還是不受控。
不要把 prop 值“鏡像”到 state,而是要讓組件受控,并且合并在一些父組件中的兩個分叉值。比如說,不是要讓子組件接收一個props.value,并且跟蹤一個草稿字段state.value,而是要讓父組件管理 state.draftValue還有state.committedValue,直接控制子組件的值。會使得數據流更明顯,更穩定。
對于不受控組件,如果你想要在一個 ID 這樣的特殊 prop 變化的時候重置 state,你會有以下選項:
推薦:為了重置所有內部state,使用 key 屬性
方案一:為了重置某些字段值,監聽一個props.userID這種特殊字段的變化
方案二:也可以會退到使用 refs 屬性的命令式實例方法
內存化我們已經看到 derived state 為了確保一個用在 render的字段而在輸入框變化時被重新計算。這項技術叫做內存化。
使用 derived state 去達到內存化并沒有那么糟糕,但是也不是最佳方案。管理 derived state 本身比較復雜,屬性變多時變得更復雜了。比如說,如果我們增加第二個 derived 字段到我們的組件 state,那么我們需要針對兩個值的變化來做追蹤。
看看一個組件例子,它有一個列表 prop,組件渲染出匹配用戶查詢輸入字符的列表選項。我們應該使用 derived state 來存儲過濾好的列表。
class Example extends Component { state = { filterText: "", } // ******************** // NOTE: this example is NOT the recommended approach. // See the examples below for our recommendations instead. // ******************** staitic getDerivedStateFromProps(nextProps, prevState) { // Re-run the filter whenever the list array or filter text change. // Note we need to store prePropsList and prevFilterText to detect change. if ( nextProps.list !== prevState.prevPropsList || prevState.prevFilterList !== prevState.filterText) { return { prevPropsList: nextProps.list, prevFilterText: prevState.filterText, filteredList: nextProps.list.filter(item => item.text.includes(prevState.filterText)) } } return null } handleChange = e => { this.setState({ filterText: e.target.value }) } render () { return () } } {this.state.filteredList.map(item =>
- {item.text}
)}
該實現避免了filteredList經常不必要的重新計算。但是也復雜了些。因為需要多帶帶追蹤 props和 state 的變化,為的是適當的更新過濾好的列表。這里,我們可以使用PureCompoennt來做簡化,把過濾操作放到 render 方法里去:
// PureCompoents only rerender if at least one stae or prop value changes. // Change is determined by doing a shallow comparison of stae and prop keys. class Example Extends PureComponent { // State only needs to hold the current filter text value: state = { filterText: "", } handleChange = e => { htis.setState({ filterText: e.target.value }) } render () { // The render method on this PureComponent is called only if // props.list or state.filterList has changed. const filteredList = this.props.list.filter( item => item.text.includes(this.stae.filterText) ) return () } } {filteredList.map(item =>
- {item.text}
)}
上面代碼要干凈多了而且比 derived state 版本要更簡單。只是偶爾不夠好:對于大列表的過濾有點慢,而且如果另一個 prop 要變化的話PureComponent不會防止重新渲染。基于這樣的考慮,我們增加了memoization helper來避免非必要的列表重新過濾:
import memoize from "memoize-one" class Example extends Component { // State only need to hold the current filter text value: state = { filterText: "" } filter = memoize( (list, filterText) => list.filter(item => item.text.includes(filterText)) ) handleChange = e => { this.setState({ filterText: e.target.value }) } render () { // Calculate the latest filtered list. If these arguments havent changed // since the last render, `"memoize-one` will reuse the last return value. const filteredList = this.filter(this.props.list, this.sate.filterText) return () } } {filteredList.map(item =>
- {item.text}
)}
這要簡單多了,而且和 derived state 版本一樣好。
當使用memoization的時候,需要滿足一些條件:
在大多數情況下,你會把內存化函數添加到一個組件實例上。這會防止該組件的多個實例重置每一個內存化屬性。
通常你使用一個帶有有限緩存大小的內存化工具,為的是防止時間累計下來的內存泄露。(在上述例子中,我們使用memoize-one因為它僅僅會緩存最近的參數和結果)。
這一節里,如果每次父組件渲染的時候props.list重新生成的話,上述實現會失效。但是在多數情況下,上述實現是合適的。
結束語在實際應用中,組件經常混合著受控和不受控的行為。理所應當。如果每個值都有明確源,你就可以避免上面的反模式。
重申一下,由于比較復雜,getDerivedStateFromProps(還有 derived state)是一項高級特性,而且應該用少點。如果你使用的時候遇到麻煩,請在 GitHub 或者 Twitter 上聯系我們。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96346.html
摘要:是強大的,你可以做很多事情沒有。如果你想要你的項目需要更少的依賴,并且你清楚的知道你的目標瀏覽器,那么你可能不需要。我們并不需要為了操作等再學習一下的。但是,他們往往需要更多的資源,功能不強,難以通過腳本自動化。 1 You-Dont-Need-JavaScript CSS是強大的,你可以做很多事情沒有JS。 本文教你使用原生CSS做下面的事情。 內容目錄 手風琴/切換 圓盤傳送帶...
摘要:是強大的,你可以做很多事情沒有。如果你想要你的項目需要更少的依賴,并且你清楚的知道你的目標瀏覽器,那么你可能不需要。我們并不需要為了操作等再學習一下的。但是,他們往往需要更多的資源,功能不強,難以通過腳本自動化。 1 You-Dont-Need-JavaScript CSS是強大的,你可以做很多事情沒有JS。 本文教你使用原生CSS做下面的事情。 內容目錄 手風琴/切換 圓盤傳送帶...
摘要:源碼函數調用過,沒有變化,參數時返回緩存值。而通過,可以把上一次的計算結果保存下來,而避免重復計算。這意味著將跳過渲染組件,并重用最后渲染的結果。 1. 基本概念 在一個CPU密集型應用中,我們可以使用Memoization來進行優化,其主要用于通過存儲昂貴的函數調用的結果來加速程序,并在再次發生相同的輸入時返回緩存的結果。例如一個簡單的求平方根的函數: const sqrt = Ma...
摘要:熱門文章我在淘寶做前端的這三年紅了櫻桃,綠了芭蕉。文章將在淘寶的三年時光折射為入職職業規劃招聘晉升離職等與我們息息相關的經驗分享,值得品讀。 showImg(https://segmentfault.com/img/remote/1460000018739018?w=1790&h=886); 【Alibaba-TXD 前端小報】- 熱門前端技術快報,聚焦業界新視界;不知不覺 2019 ...
摘要:熱門文章我在淘寶做前端的這三年紅了櫻桃,綠了芭蕉。文章將在淘寶的三年時光折射為入職職業規劃招聘晉升離職等與我們息息相關的經驗分享,值得品讀。 showImg(https://segmentfault.com/img/remote/1460000018739018?w=1790&h=886); 【Alibaba-TXD 前端小報】- 熱門前端技術快報,聚焦業界新視界;不知不覺 2019 ...
閱讀 1241·2021-11-08 13:25
閱讀 1440·2021-10-13 09:40
閱讀 2774·2021-09-28 09:35
閱讀 735·2021-09-23 11:54
閱讀 1123·2021-09-02 15:11
閱讀 2431·2019-08-30 13:18
閱讀 1668·2019-08-30 12:51
閱讀 2686·2019-08-29 18:39