原文连接: 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
更稳定,滥用状况会减小一些。html
本文说起的全部反模式案例面向旧钩子函数componentWillReceiveProps
和新钩子函数getDerivedStateFromProps
。
本文会涵盖下面讨论:react
一些 derived state 的常见 buggit
getDerivedStateFromProps
存在的惟一目的是使得组件在 props 改变时能都更新好内在state。咱们以前的博文有过一些例子,好比基于一个变化着的偏移 prop 来记录当前滚动方向或者根据一个来源 prop 来加载外部数据。github
咱们没有给出许多例子,由于整体原则上来说,derived state 应该用少点。咱们见过的全部derived state 的问题大多数能够归结为,要么没有任何前提条件的从 props 更新state,要么 props,state 不匹配的任什么时候候去更新 state。(咱们将在下面谈及更多细节)算法
受控,不受控概念一般针对表单输入,可是也能够用来描述组件的数据活动。props 传递进来的数据能够当作受控的(由于父组件控制了数据源)。组件内部状态的数据能够当作不受控的(由于组件能直接改变他)。数组
最多见的derived state错误 就是混淆二者(受控,不受控数据);当一个 state 的变动字段也能够经过 setState 调用来更新的时候,就没有一个单一的(真相)数据源。上面谈及的加载外部数据的例子可能听起来状况相似,可是一些重要方面仍是不同的。在加载例子中,source 属性和 loading 状态有着一个清晰数据源。当source prop改变的时候,loading 状态老是被重写。相反,loading 状态只会在 prop 改变的时候被重写,其余状况下就是被组件管控着。缓存
问题就是在这些约束变化的时候出现的。最典型的两种形式以下,咱们来瞧瞧:安全
一个常见的误解就是觉得getDerivedStateFromProps
和componentWillReceivedProps
会只在props 改变的时候被调用。实际上这两个钩子函数可能在父组件渲染的任什么时候候被调用,无论 props 是否是和之前不一样。所以,用这两个钩子函数来无条件消除 state 是不安全的。这样作会使得 state 更新丢失。性能优化
咱们看看一个范例,这是一个邮箱输入组件,镜像了一个 email prop 到 state:app
class EmailInput extends Component { state = { email: this.props.email } render () { return <input onChange={this.handleChange} value={this.state.email} /> } 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 <input onChange={props.onChange} value={props.email} /> }
这个办法简化了组件的实现,若是咱们仍然想要保存草稿值的话,父表单组件将须要手动处理。这有一个这种模式的demo。
另外一个方案就是咱们的组件须要彻底控制 draft 邮箱状态值。这样的话,组件仍然能够接受一个prop初始值,可是会忽略该prop 的连续变化:
class EmailInput extends Component { state = { email: this.props.defaultEmail } handleChange = e => { this.setState({ email: e.target.value }) } render () { return <input onChange={this.handleChange} value={this.state.email} /> } }
在聚焦到另外一个表单项的时候为了重置邮箱值(好比密码控件场景),咱们可使用React 的 key 属性。当 key 变化时,React 会建立一个新组件实例,而不是更新当前组件。Keys 一般对于动态列表颇有用,不过在这里也颇有用。在一个新用户选中时,咱们用 user ID 来从新建立一个表单输入框:
<EmailInput defaultEmail={this.props.user.email} key={this.props.user.id} />
每次 ID 改变的时候,EmailInput
输入框都会从新生成,它的 state 也就会重置到最新的 defaultEmail
值。栗子不能少,这个方案下,没有必要把 key 值添加到每一个输入框。在整个form表单上 添加一个 key 属性或许会更合理。每次 key 变化时,表单内的全部组件都会从新生成,同时初始化 state。
在大多数状况,这是处理须要重置的state的最佳办法。
这个办法可能听起来性能慢,可是实际表现上可能微不足道。若是一个组件有复杂更新逻辑的话使用key属性可能会更快,由于diffing算法走了弯路
若是 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,你会有如下选项:
props.userID
这种特殊字段的变化咱们已经看到 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 ( <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 的变化,为的是适当的更新过滤好的列表。这里,咱们可使用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 ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ) } }
上面代码要干净多了并且比 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 ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ) } }
这要简单多了,并且和 derived state 版本同样好。
当使用memoization
的时候,须要知足一些条件:
memoize-one
由于它仅仅会缓存最近的参数和结果)。props.list
从新生成的话,上述实现会失效。可是在多数状况下,上述实现是合适的。在实际应用中,组件常常混合着受控和不受控的行为。理所应当。若是每一个值都有明确源,你就能够避免上面的反模式。
重申一下,因为比较复杂,getDerivedStateFromProps
(还有 derived state)是一项高级特性,并且应该用少点。若是你使用的时候遇到麻烦,请在 GitHub 或者 Twitter 上联系咱们。