You Probably Dont Need Derived State

原文连接: 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
  • 一些 derived state 的常见 buggit

    • 反模式:无条件地拷贝props 到state
    • 反模式:当 props 改变的时候清除 state
  • 建议解决方案
  • 内存化

何时去使用Derived State

getDerivedStateFromProps存在的惟一目的是使得组件在 props 改变时能都更新好内在state。咱们以前的博文有过一些例子,好比基于一个变化着的偏移 prop 来记录当前滚动方向或者根据一个来源 prop 来加载外部数据。github

咱们没有给出许多例子,由于整体原则上来说,derived state 应该用少点。咱们见过的全部derived state 的问题大多数能够归结为,要么没有任何前提条件的从 props 更新state,要么 props,state 不匹配的任什么时候候去更新 state。(咱们将在下面谈及更多细节)算法

  • 若是你正在使用 derived state 来进行一些基于当前 props 的内存化计算,那么你不须要 derived state。memoization 小节会细细道来。
  • 若是你在无条件地更新 derived state或者 props,state 不匹配的时候去更新它,你的组件极可能太频繁地重置 state,继续阅读可见分晓。

derived state 的常见 bug

受控,不受控概念一般针对表单输入,可是也能够用来描述组件的数据活动。props 传递进来的数据能够当作受控的(由于父组件控制了数据源)。组件内部状态的数据能够当作不受控的(由于组件能直接改变他)。数组

最多见的derived state错误 就是混淆二者(受控,不受控数据);当一个 state 的变动字段也能够经过 setState 调用来更新的时候,就没有一个单一的(真相)数据源。上面谈及的加载外部数据的例子可能听起来状况相似,可是一些重要方面仍是不同的。在加载例子中,source 属性和 loading 状态有着一个清晰数据源。当source prop改变的时候,loading 状态老是被重写。相反,loading 状态只会在 prop 改变的时候被重写,其余状况下就是被组件管控着。缓存

问题就是在这些约束变化的时候出现的。最典型的两种形式以下,咱们来瞧瞧:安全

反模式: 无条件的从 props 拷贝至 state

一个常见的误解就是觉得getDerivedStateFromPropscomponentWillReceivedProps会只在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

推荐方案二: 带有 key 属性的全不受控组件

另外一个方案就是咱们的组件须要彻底控制 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算法走了弯路
  • 方案一:经过 ID 属性重置 uncontrolled 组件

若是 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,你会有如下选项:

  • 推荐:为了重置全部内部state,使用 key 属性
  • 方案一:为了重置某些字段值,监听一个props.userID这种特殊字段的变化
  • 方案二:也能够会退到使用 refs 属性的命令式实例方法

内存化

咱们已经看到 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的时候,须要知足一些条件:

  1. 在大多数状况下,你会把内存化函数添加到一个组件实例上。这会防止该组件的多个实例重置每个内存化属性。
  2. 一般你使用一个带有有限缓存大小的内存化工具,为的是防止时间累计下来的内存泄露。(在上述例子中,咱们使用memoize-one由于它仅仅会缓存最近的参数和结果)。
  3. 这一节里,若是每次父组件渲染的时候props.list从新生成的话,上述实现会失效。可是在多数状况下,上述实现是合适的。

结束语

在实际应用中,组件常常混合着受控和不受控的行为。理所应当。若是每一个值都有明确源,你就能够避免上面的反模式。

重申一下,因为比较复杂,getDerivedStateFromProps(还有 derived state)是一项高级特性,并且应该用少点。若是你使用的时候遇到麻烦,请在 GitHub 或者 Twitter 上联系咱们。

相关文章
相关标签/搜索