摘要:所有派生狀態導致的問題無異于兩種無條件的根據來更新無論和是否匹配來更新。派生狀態最常見的錯誤就是將這兩者混和在一起。因此通常被用于性能優化而不是來判斷派生狀態的正確性。我們可以使用派生狀態來存儲過濾列表這種方式避免了重新計算。
原文鏈接:https://reactjs.org/blog/2018...
翻譯這篇文章的起因是因為在一次需求迭代中錯誤的使用了getDerivedStateFromProps這個生命周期導致子組件的state被循環重置,于是翻到了這篇文章,然后就開啟的翻譯之旅。
在很長一段時間,生命周期componentWillReceiveProps是用來響應props更新來改變state并不需要額外渲染的唯一方法。在16.3版本中,我們提供了getDerivedStateFromProps這個更安全生命周期來解決相同的用例。同時,我們發現人們對于如何使用這兩種方式有很多誤解,并且我們發現了一些造成微妙和令人混淆的反模式。在16.4中的getDerivedStateFromProps的bug修復使得派生狀態更加可預測,且更容易讓人注意到錯誤使用它的結果。
什么時候去使用派生狀態getDerivedStateFromProps的存在只有一個目的。它可以使組件根據props的改變來更新內部的state。我們之間的博客提供了一些例子:通過改變offset的prop來改變當前的滾動方向和加載通過source props所指定的外部數據。
我們沒有提供更多的例子,因為作為一個基本的規則,派生狀態應該被謹慎的使用。所有派生狀態導致的問題無異于兩種:(1)無條件的根據props來更新state(2)無論props和state是否匹配來更新state。
如果僅用派生狀態來記錄一些基于當前props的計算,則不需要派生狀態;
如果你無條件的更新派生狀態,或者無論props和state是否匹配來更新state,你的組件將會過于頻繁的去重置狀態;
使用派生狀態的常見問題“受控的”和“不受控的”通常用來指表單的輸入,但它也同樣可以表示任何組件數據所在的位置。數據通過props傳來被認為是“受控的”(因為父組件在控制著這個數據)。數據僅存在其內部的state中被認為是“不受控的”(因為其父組件不能直接的改變這它)。
派生狀態最常見的錯誤就是將這兩者混和在一起。當一個派生狀態的值同樣通過setState的調用來更新時,這就無法保證數據有單一的真實來源。這也許和上面提到的外部數據加載的例子很相似,但他們在一些重要的方面上是不同的。在加載的例子中,”source“的props和”loading“的state都有一個明確的真實來源。當source props改變的時候,應該總是覆蓋loading state。相反,只有props改變且由組件管理的時候,才去重寫state。
當這些約束中的任何一個被改變時將會出現問題。通常有兩種形式,讓我們接下來看一下這兩種形式。
反模式:無條件的從prop復制狀態到state一個常見的誤解是getDerivedStateFromProps和componentWillReceiveProps只有在props改變的時候會被調用。這兩個生命周期將會在父組件重新渲染的任何時間被調用,而不管props是否與之前不同。因此,在使用這兩個生命周期時,無條件的覆蓋state總是不安全的,將會導致state更新時的丟失。
讓我們考慮一個例子來說明這個問題。
class EmailInput extends Component { state = { email: this.props.email }; render() { return ; } handleChange = event => { this.setState({ email: event.target.value }); }; componentWillReceiveProps(nextProps) { // 這里將會覆蓋任何本地state的更新。 this.setState({ email: nextProps.email }); } }
這個組件看起來可能沒有問題,state由prop傳來的數據初始化,且當我們改變input的值時state被更新。但是當我們的父組件重新渲染的時候,任何我們在input中輸入的狀態都將丟失,即使我們去比較nextProps.email !== this.state.email也是如此。
在這個例子中,只有當email的prop的改變的時候添加shouldComponentUpdate來重新渲染可以解決這個問題,但是實際上,組件通常會接收多個prop,另一個prop的改變任然會造成組件的重新渲染和不正確的重置。此外函數和對象的prop通常也會是內聯創建的,這也會使shouldComponentUpdate正確的返回true變得困難。這里有一個例子。因此shouldComponentUpdate通常被用于性能優化而不是來判斷派生狀態的正確性。
希望到現在大家清楚為什么不要無條件的復制props到state。在我們找到可能的解決方案之前,讓我們去看一個與之相關的問題:如果只在props.email改變的時候去更新state會怎樣?
反模式:props改變的時候清除state繼續上面的例子,我們可以避免在props.email更改時意外的清除state:
class EmailInput extends Component { state = { email: this.props.email }; componentWillReceiveProps(nextProps) { // 任何時候props.email改變,更新state. if (nextProps.email !== this.props.email) { this.setState({ email: nextProps.email }); } } // ... }
我們取得了很大的進步,現在我們的組件只有在props真正改變的時候才會清除state。
還有一個微妙的問題,想象一下使用以上的組件來構建密碼管理應用。當使用同一個email在兩個賬戶的詳情頁導航時,input將會無法重置,這是因為傳遞給組件的props相對于兩個賬號來說時相同的。這對用戶來說將會是一個驚喜,因為對一個賬戶的未保存更改會錯誤的影響到另一個賬戶。查看演示。
這種設計從本質上來說是錯誤的,但卻是一個很容易犯的錯誤,幸運的是,有兩種更好的選擇,這兩者的關鍵在于,對于任何數據片段,你都需要選擇一個將它作為數據源的組件,而避免在其它組件重復使用。
首選方案 推薦:完全受控組件避免上述問題的一個方案是完全移除組建中的state,如果email僅作為props存在,那我們將不必擔心它和state沖突,我們甚至可以講EmailInput組件變為更輕量級的function組件:
function EmailInput(props) { return ; }
這種方法簡化了組件的實現,但如果你仍需要存儲一個草稿值,那么父表單組件現在需要手動執行該操作。查看演示。
推薦:帶有key的完全不受控組件另一個方法是讓我們的組件完全擁有“草稿”email的state,這時我們的組件仍然可以接收props來作為初始值,但是它會忽略props的后續更改。
class EmailInput extends Component { state = { email: this.props.defaultEmail }; handleChange = event => { this.setState({ email: event.target.value }); }; render() { return ; } }
為了在移動到其他項目時重置值(如密碼管理器場景中),可以使用一個React的特殊屬性key。當一個key改變的時候,React會創建一個新的組件實例而不是更新當前的組件,key通常被用在動態的list但是同樣可以在這里使用。
每當id改變的時候,EmailInput組件將會被重新創建,它的state將會被重置為最后一次的defaultEmail的值。查看演示。使用此方法,你講不用在每一個input上添加key,把一個key放在整個form上更有意義,每當key改變的時候,表單中的input都會重置到其初始狀態。
替代方案1:通過ID prop來重置不受控組件如果key在某些場合不適用(也許初始化對于組件來說是昂貴的),一個可行但繁瑣的方式是在getDerivedStateFromProps中去監測userID:
class EmailInput extends Component { state = { email: this.props.defaultEmail, prevPropsUserID: this.props.userID }; static getDerivedStateFromProps(props, state) { if (props.userID !== state.prevPropsUserID) { return { prevPropsUserID: props.userID, email: props.defaultEmail }; } return null; } // ... }
這也提供了靈活性,如果我們選擇,只重置組件內的部分state。查看演示。
替代方案2:通過實例方法來重置不受控組件如果沒有合適的id來作為key但是又要重置狀態,一種解決方案是為組件生成一個隨機數或者自動遞增值來作為key,另一種方案是通過實例的方法來強制重置組件的state。
class EmailInput extends Component { state = { email: this.props.defaultEmail }; resetEmailForNewUser(newEmail) { this.setState({ email: newEmail }); } // ... }
父組件將通過ref拿到組件的實例從而調用該方法。查看演示。
在某些場景下ref會很有用,但是我們建議你謹慎的使用它,即使在demo中,這個方法也是最不理想的,因為將會造成兩次渲染而不是一個。
總結總而言之,當設計一個組件時,一個重要的方面是它的數據是可控的還是不可控的。
盡量避免在state中去“鏡像”一個props值,使這個組件成為受控組件,在父組件的state中去合并這兩個state。例如,與其在組件中去接受一個committed的props并且跟蹤一個draft的state,不如讓父組件去同時管理這個state.draftValue和state.committedValue并直接控制子組件,這將使組件更加的明確和可預測。
對于一個不受控組件,如果你想根據一個props的改變來重置state,你需要遵循以下幾點:
首選:要重置全部內部state,使用key屬性;
備選1:如果只重置部分state,監測props中屬性的變化;
備選2:還可以考慮通過ref調用實力的方法;
memoization怎樣?我們還看到了派生狀態用于確保渲染中使用的昂貴值僅在輸入發生變化時才會重新計算,這種技術叫做memoization
使用派生狀態來做memoization不一定是壞事,但通常不是最好的解決辦法。派生狀態的管理存在一定的復雜性,并且這種復雜性隨著屬性的增加而增加。例如,如果我們向組件的state添加第二個派生字段,那么我們的實現將需要分別跟蹤對兩個字段的更改。
讓我們看一個組件的示例,該組件使用一個prop(項目列表)并呈現與用戶輸入的搜索查詢匹配的項。我們可以使用派生狀態來存儲過濾列表:
class Example extends Component { state = { filterText: "", }; // ******************************************************* // NOTE: this example is NOT the recommended approach. // See the examples below for our recommendations instead. // ******************************************************* static getDerivedStateFromProps(props, state) { // Re-run the filter whenever the list array or filter text change. // Note we need to store prevPropsList and prevFilterText to detect changes. if ( props.list !== state.prevPropsList || state.prevFilterText !== state.filterText ) { return { prevPropsList: props.list, prevFilterText: state.filterText, filteredList: props.list.filter(item => item.text.includes(state.filterText)) }; } return null; } handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { return (); } } {this.state.filteredList.map(item =>
- {item.text}
)}
這種方式避免了重新計算filteredList。但是他比我們需要的更加的復雜,因為它需要分別的跟蹤和檢查我們的props和state以便能夠正確的更新列表。在下面這個例子中,我們通過PureComponent并將filter操作放到render中來簡化操作:
// PureComponents只有在至少一個state或者prop改變的時候才會重新渲染 // 通過對state和props的keys的淺比較來確認改變。 class Example extends PureComponent { state = { filterText: "" }; handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { // 只有props.list 或 state.filterText 改變的時候PureComponent的render才會調用 const filteredList = this.props.list.filter( item => item.text.includes(this.state.filterText) ) return (); } } {filteredList.map(item =>
- {item.text}
)}
上述例子比派生狀態的版本更加的干凈和簡潔,但是有些時候這可能還不夠好,例如對于大型列表來說,過濾可能很慢,且如果有其他的props改變PureComponent也不會阻止其重新渲染。為了解決這兩個問題,我們可以添加一個memoization,以避免不必要地重新過濾我們的列表:
import memoize from "memoize-one"; class Example extends Component { state = { filterText: "" }; filter = memoize( (list, filterText) => list.filter(item => item.text.includes(filterText)) ); handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { const filteredList = this.filter(this.props.list, this.state.filterText); return (); } } {filteredList.map(item =>
- {item.text}
)}
當使用memoization時,有以下約束:
在大多數情況下,您需要將memoized函數附加到組件實例。這可以防止組件的多個實例重置彼此的memoized key。
通常情況下,您需要使用具有有限緩存大小的memoization,以防止內存泄漏。(在上面的例子中,我們使用了memoize-one,因為它只緩存最近的參數和結果。)
如果每次父組件呈現時重新創建props.list,本節中顯示的實現都不會起作用。但在大多數情況下,這種設置是合適的。
最后在實際應用中,組件通常包含受控和不受控制行為混合。沒關系,如果每個值都有明確的來源,則可以避免上面提到的反模式。
值得重新思考的是,getDerivedStateFromProps(以及通常的派生狀態)是一種高級功能,應該謹慎使用。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/99313.html
摘要:譯者前端小智原文就像人們對更新移動應用程序和操作系統感到興奮一樣,開發人員也應該對更新框架感到興奮。錯誤邊界是一種組件。注意將作為值傳遞進去并不會導致使用。如果兩者不同,則返回一個用于更新狀態的對象,否則就返回,表示不需要更新狀態。 譯者:前端小智 原文:medium.freecodecamp.org/why-react16… 就像人們對更新移動應用程序和操作系統感到興奮一樣,開發人員也應...
摘要:我現在寫的這些是為了解決和這兩個狀態管理庫之間的困惑。這甚至是危險的,因為這部分人將無法體驗和這些庫所要解決的問題。這肯定是要第一時間解決的問題。函數式編程是不斷上升的范式,但對于大部分開發者來說是新奇的。規模持續增長的應 原文地址:Redux or MobX: An attempt to dissolve the Confusion 原文作者:rwieruch 我在去年大量的使用...
摘要:開閉原則軟件實體類,模塊,函數應該是可以擴展的,而不是修改。函數并不符合開閉原則,因為一旦有新動物出現,它需要修改代碼。 By Chidume Nnamdi | Oct 9, 2018 原文 面向對象的編程類型為軟件開發帶來了新的設計。 這使開發人員能夠在一個類中組合具有相同目的/功能的數據,來實現單獨的一個功能,不必關心整個應用程序如何。 但是,這種面向對象的編程還是會讓開發者困惑或...
繼承 在前面的課程中,你已經多次看到了繼承,在Java語言中,類可以從其他類派生,從而從這些類繼承字段和方法。 定義:從另一個類派生的類稱為子類(也是派生類,擴展類或子類),派生子類的類稱為超類(也是基類或父類)。 除了Object沒有超類,每個類都有一個且只有一個直接超類(單繼承),在沒有任何其他顯式超類的情況下,每個類都隱式地是Object的子類。 類可以從派生自類的類派生的類派生,依此類推,...
閱讀 3649·2023-04-26 02:32
閱讀 3954·2021-11-23 10:05
閱讀 2306·2021-10-08 10:04
閱讀 2733·2021-09-22 16:06
閱讀 3628·2021-09-22 15:27
閱讀 777·2019-08-30 15:54
閱讀 1730·2019-08-30 13:50
閱讀 2714·2019-08-29 13:56