原文連接:reactjs.org/blog/2018/0…javascript
翻譯這篇文章的原由是由於在一次需求迭代中錯誤的使用了getDerivedStateFromProps這個生命週期致使子組件的state被循環重置,因而翻到了這篇文章,而後就開啓的翻譯之旅。html
在很長一段時間,生命週期componentWillReceiveProps
是用來響應props更新來改變state並不須要額外渲染的惟一方法。在16.3版本中,咱們提供了getDerivedStateFromProps
這個更安全生命週期來解決相同的用例。同時,咱們發現人們對於如何使用這兩種方式有不少誤解,而且咱們發現了一些形成微妙和使人混淆的反模式。在16.4中的getDerivedStateFromProps
的bug修復使得派生狀態更加可預測,且更容易讓人注意到錯誤使用它的結果。java
getDerivedStateFromProps的存在只有一個目的。它可使組件根據props的改變來更新內部的state。咱們之間的博客提供了一些例子:經過改變offset的prop來改變當前的滾動方向和加載經過source props所指定的外部數據。react
咱們沒有提供更多的例子,由於做爲一個基本的規則,派生狀態應該被謹慎的使用。全部派生狀態致使的問題無異於兩種:(1)無條件的根據props來更新state(2)不管props和state是否匹配來更新state。緩存
「受控的」和「不受控的」一般用來指表單的輸入,但它也一樣能夠表示任何組件數據所在的位置。數據經過props傳來被認爲是「受控的」(由於父組件在控制着這個數據)。數據僅存在其內部的state中被認爲是「不受控的」(由於其父組件不能直接的改變這它)。安全
派生狀態最多見的錯誤就是將這二者混和在一塊兒。當一個派生狀態的值一樣經過setState的調用來更新時,這就沒法保證數據有單一的真實來源。這也許和上面提到的外部數據加載的例子很類似,但他們在一些重要的方面上是不一樣的。在加載的例子中,」source「的props和」loading「的state都有一個明確的真實來源。當source props改變的時候,應該老是覆蓋loading state。相反,只有props改變且由組件管理的時候,纔去重寫state。性能優化
當這些約束中的任何一個被改變時將會出現問題。一般有兩種形式,讓咱們接下來看一下這兩種形式。bash
一個常見的誤解是getDerivedStateFromProps
和componentWillReceiveProps
只有在props改變的時候會被調用。這兩個生命週期將會在父組件從新渲染的任什麼時候間被調用,而無論props是否與以前不一樣。所以,在使用這兩個生命週期時,無條件的覆蓋state老是不安全的,將會致使state更新時的丟失。markdown
讓咱們考慮一個例子來講明這個問題。app
class EmailInput extends Component { state = { email: this.props.email }; render() { return <input onChange={this.handleChange} value={this.state.email} />; } 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.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 <input onChange={props.onChange} value={props.email} />; } 複製代碼
這種方法簡化了組件的實現,但若是你仍須要存儲一個草稿值,那麼父表單組件如今須要手動執行該操做。查看演示。
另外一個方法是讓咱們的組件徹底擁有「草稿」email的state,這時咱們的組件仍然能夠接收props來做爲初始值,可是它會忽略props的後續更改。
class EmailInput extends Component { state = { email: this.props.defaultEmail }; handleChange = event => { this.setState({ email: event.target.value }); }; render() { return <input onChange={this.handleChange} value={this.state.email} />; } } 複製代碼
爲了在移動到其餘項目時重置值(如密碼管理器場景中),可使用一個React的特殊屬性key
。當一個key改變的時候,React會建立一個新的組件實例而不是更新當前的組件,key
一般被用在動態的list可是一樣能夠在這裏使用。
<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>
複製代碼
每當id改變的時候,EmailInput組件將會被從新建立,它的state將會被重置爲最後一次的defaultEmail的值。查看演示。使用此方法,你講不用在每個input上添加key
,把一個key
放在整個form上更有意義,每當key
改變的時候,表單中的input都會重置到其初始狀態。
若是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。查看演示。
若是沒有合適的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,你須要遵循如下幾點:
key
屬性;ref
調用實力的方法;咱們還看到了派生狀態用於確保渲染中使用的昂貴值僅在輸入發生變化時纔會從新計算,這種技術叫作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 ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); } } 複製代碼
這種方式避免了從新計算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 ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); } } 複製代碼
上述例子比派生狀態的版本更加的乾淨和簡潔,可是有些時候這可能還不夠好,例如對於大型列表來講,過濾可能很慢,且若是有其餘的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 ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); } } 複製代碼
當使用memoization時,有如下約束:
在實際應用中,組件一般包含受控和不受控制行爲混合。不要緊,若是每一個值都有明確的來源,則能夠避免上面提到的反模式。
值得從新思考的是,getDerivedStateFromProps
(以及一般的派生狀態)是一種高級功能,應該謹慎使用。