很长一段时间,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
的用法值得被从新思考,由于他是一个拥有必定复杂度的高级特性,咱们应该谨慎地使用。