很長一段時間,componentWillReceiveProps
生命週期是在不進行額外render的前提下,響應props中的改變並更新state的惟一方式。在16.3版本中,咱們介紹了一個新的替代生命週期getDerivedStateFromProps去更安全地解決相同的問題。同時,咱們意識到人們對這兩個方式都存在不少的誤解,咱們發現了其中的一些反例觸發了一些奇怪的bug。在本次版本中咱們修復了它,並讓derived state更加可預測,因此咱們能更容易地注意到濫用的結果。html
本文的反例既包含老的componentWillReceiveProps也包含新的getDerivedStateFromProps方法react
getDerivedStateFromProps
只爲了一個目的存在。它使得一個組件可以響應props的變化來更新本身內部的state。好比咱們以前提到的根據變化的offset屬性記錄目前的滾動方向或者根據source屬性加載額外的數據。git
咱們提供了許多了實例,由於通常來講,派生狀態應該被謹慎地使用。咱們見過的全部關於派生狀態的問題最後均可以被歸爲兩種:(1)從props那裏無條件地更新state(2)當props和state不匹配的時候更新state(咱們在下面會深刻探討)github
關於緩存記憶(memoization)
。"受控"和"非受控"一般指表明單的輸入控件,可是它還能夠用於描述組件的數據所處位置。經過props傳入的數據可被稱爲受控的(由於父組件控制這數據)。只存在內部state的數據被稱做非受控的(由於父組件不能直接改變它)。數組
最多見的錯誤是將二者搞混了。當一個派生狀態同時被setState
更新的時候,數據就失去了單一的事實來源。上面提到的加載數據的例子看上去是相似的,但在一些關鍵的地方是有區別的。在例子中,每當source屬性變化,loading狀態一定會被覆蓋。反過來,狀態要麼在props變化的時候被覆蓋,要麼由組件本身管理。(譯註:可理解爲同時只有單一的真實來源)緩存
當任何一個限制被改變的時候就會發生問題,下面舉了兩個典型的例子。安全
一個常見的誤解是getDerivedStateFromProps
和componentWillReceiveProps
只會在props「改變」的時候調用。這些生命週期會在任何父組件發生render的時候調用,無論props是否真的改變。所以,使用這些週期去無條件地覆蓋state是不安全的。這樣作會使得state丟失更新。app
咱們來演示一下這個問題。這是一個郵件輸入
組件,它「映射」了email屬性到state:async
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) { // 這樣會抹去全部的內部狀態更新! // 不要這樣作. this.setState({ email: nextProps.email }); } } 複製代碼
看上去這個組件好像沒問題。state被props中的值初始化,而後隨着<input>
的輸入而更新。可是若是父組件發生render,咱們在<input>
中輸入的東西都會消失!(參見這裏的demo)即便咱們在重置前比較nextProps.email !== this.state.email
也會如此。函數
在這個簡單的例子中,添加shouldComponentUpdate
去限制只在props中的email發生改變時纔去從新render能解決這個問題。但在實際中,組件一般會接受不少的props。你沒法避免其餘的屬性發生改變。函數和對象屬性一般是內聯建立的,這讓咱們很難去實現判斷是否發生了實質性的變化。這裏有一個demo來講明。所以,shouldComponentUpdate
最好只是用來優化性能,而不是去確保派生狀態的正確性。
但願如今咱們能弄清楚爲何無條件地複製props到state是壞主意。在查看可能的解決方案前,咱們先來看一個相關的例子:若是咱們只在email屬性發生改變的時候更新state呢?
繼續上面的例子,咱們能夠經過只在props.email
改變時更新state來避免意外地覆蓋已有的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
});
}
}
// ...
}
複製代碼
儘管上面的例子中使用的是
componentWillReceiveProps
,但這個反例一樣的也對getDerivedStateFromProps
適用
咱們剛邁進了一大步。如今咱們的組件只會在props真正改變的時候覆蓋掉咱們輸入的東西了。
固然這裏仍是有一個微妙的問題。想象一個密碼管理app使用瞭如上的輸入組件。當切換兩個不一樣的帳號的時候,若是這兩個帳號的郵箱相同,那麼咱們的重置就會失效。由於對於這兩個帳戶傳入的email屬性是同樣的。(譯註:比如你切換了一份數據源,可是這兩份數據中的email是相等的,因而預期應該被重置的輸入框沒有被重置)查看demo
這個設計從根本上是有缺陷的,但倒是很容易犯的錯誤。(我本身也曾犯過)幸運的是有兩個更好的可選方案。關鍵在於對於任何數據,你須要選擇一個做爲其真實來源的組件,而且避免在其餘組件中複製它。咱們來看看下面的解決方案。
一個解決上述問題的方案是徹底移除咱們組件中的state。若是email僅做爲屬性存在,咱們就不須要擔憂和state的衝突。咱們甚至能夠把EmailInput
做爲一個更輕量級的函數組件:
function EmailInput(props) {
return <input onChange={props.onChange} value={props.email} />; } 複製代碼
此舉簡化了咱們組件的實現,可是若是咱們仍然想保存一份輸入的草稿值呢,這時候須要父組件手動來實現了,請看demo
另外一個可選方案是咱們的組件徹底控制eamil的「草稿」狀態。在這裏,咱們的組件仍然接受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通常用在動態列表,可是在這也是頗有用的。這裏當選擇一個新用戶的時候,咱們用用戶ID去從新建立這個email輸入組件:
<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>
複製代碼
每當ID改變,EmailInput
會被從新建立而後state會被重置爲最新的defaultEmail
值。(點這查看demo)其實你無需爲每個輸入框添加一個key。對整個表單添加一個key
會顯得更有用。每當key改變,表單的全部組件都會被重建且被賦上乾淨的初始值。
大多數狀況,這是重置state最好的辦法。
從新建立組件聽上去會很慢,但其實對性能的影響微乎其微。若是組件具備不少更新上的邏輯,則使用key甚至能夠更快,由於該子樹的差別得以被繞過。
若是由於某些緣由沒法使用key
(好比組件初始化的代價很高),一個可行但笨重的辦法是在getDerivedStateFromProps
監聽「userID」的改變。
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// 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 (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
// ...
}
複製代碼
若是咱們這樣選擇,這也提供了僅重置部件的內部狀態的靈活性(這裏查看demo)
上面的例子對於componentWillReceiveProps也是同樣的
在極少狀況,你可能須要在沒有合適的ID做爲key的狀況下重置state。一個辦法是把key設爲隨機值或者遞增的值,在你想要重置的時候改變它。另外一個可行方案是暴露一個實例方法命令式地去改變內部的state:
class EmailInput extends Component {
state = {
email: this.props.defaultEmail
};
resetEmailForNewUser(newEmail) {
this.setState({ email: newEmail });
}
// ...
}
複製代碼
父表單組件就能夠經過ref去調用這個方法,(點擊這裏查看demo)
Ref這這類場景中挺有用,但咱們推薦你儘可能謹慎地去使用。即便在這個例子中這種方法也是不理想的,由於會造觸發兩次render。
咱們也見到過用派生狀態來確保render
中計算量較大的值僅在輸入改變的時候從新計算。這個技術叫作memoization。
使用它做緩存記憶不必定是很差的,但一般不是最佳解決方案。管理派生狀態有必定的複雜度,這個複雜度還會隨着額外的屬性而增長。好比,若是咱們給組件添加了第二個派生字段,那麼咱們須要分別跟蹤這兩個字段的變化。
咱們來看一個例子,咱們把一個列表傳入這個組件,而後它須要按用戶的輸入篩選顯示出匹配的項。咱們可使用派生狀態去保存篩選後的列表。
class Example extends Component {
state = {
filterText: "",
};
// *******************************************************
// NOTE: 這個例子不是咱們推薦的作法
// 推薦的方法參見下面的例子.
// *******************************************************
static getDerivedStateFromProps(props, state) {
// 每當列表數組或關鍵字變化時篩選列表.
// 注意到咱們須要儲存prevPropsList和prevFilterText來監聽變化.
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
來簡化操做而後把篩選操做放到render方法中去:
// PureComponents只會在至少state和props中有一個屬性發生變化時渲染.
// 變化是經過引用比較來判斷的.
class Example extends PureComponent {
// State only needs to hold the current filter text value:
state = {
filterText: ""
};
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// 只有props.list 或 state.filterText 改變時纔會調用.
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> ); } } 複製代碼
上面的方法比派生狀態的版本更簡單幹淨。然而這也不是個好方法,由於對於長列表來講可能比較慢,並且PureComponent
沒法阻止其餘屬性改變形成的render。爲了應對這種狀況咱們引入了一個memoization輔助器來避免多餘的篩選。
import memoize from "memoize-one";
class Example extends Component {
// State只須要去維護目前的篩選關鍵字:
state = { filterText: "" };
// 當列表數組或關鍵字變化時從新篩選
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// 計算渲染列表時,若是參數同上次計算沒有改變,`memoize-one`會複用上次返回的結果
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> ); } } 複製代碼
這樣更簡單地實現了和派生狀態同樣的功能!
在實際應用中,組件一般既包含受控與非受控的元素。這沒問題,若是每一個數據都有清晰的真實來源,你就能夠避開上面提到的反例。
getDerivedStateFromProps
的用法值得被從新思考,由於他是一個擁有必定複雜度的高級特性,咱們應該謹慎地使用。