资讯专栏INFORMATION COLUMN

You Probably Dont Need Derived State

URLOS / 1275人阅读

摘要:同时,我们意识到人们对于这两个钩子函数的使用有许多误解,也发现了一些造成这些晦涩的反模式。注意事项本文提及的所有反模式案例面向旧钩子函数和新钩子函数。因此,用这两个钩子函数来无条件消除是不安全的。

原文链接: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更稳定,滥用情况会减少一些。

注意事项
本文提及的所有反模式案例面向旧钩子函数componentWillReceiveProps和新钩子函数getDerivedStateFromProps

本文会涵盖下面讨论:

什么时候去使用 derived state

一些 derived state 的常见 bug

反模式:无条件地拷贝props 到state

反模式:当 props 改变的时候清除 state

建议解决方案

内存化

什么时候去使用Derived State

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

我们没有给出许多例子,因为总体原则上来讲,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:

class EmailInput extends Component {
  state = { email: this.props.email }

  render () {
    return 
  }

  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 
}

这个办法简化了组件的实现,如果我们仍然想要保存草稿值的话,父表单组件将需要手动处理。这有一个这种模式的demo。

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

另一个方案就是我们的组件需要完全控制 draft 邮箱状态值。这样的话,组件仍然可以接受一个prop初始值,但是会忽略该prop 的连续变化:

class EmailInput extends Component {
  state = { email: this.props.defaultEmail }

  handleChange = e => {
    this.setState({ email: e.target.value })
  }

  render () {
    return 
  }
}

在聚焦到另一个表单项的时候为了重置邮箱值(比如密码控件场景),我们可以使用React 的 key 属性。当 key 变化时,React 会创建一个新组件实例,而不是更新当前组件。Keys 通常对于动态列表很有用,不过在这里也很有用。在一个新用户选中时,我们用 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 (
      
        
        
    {this.state.filteredList.map(item =>
  • {item.text}
  • )}
) } }

该实现避免了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 (
      
        
        
    {filteredList.map(item =>
  • {item.text}
  • )}
) } }

上面代码要干净多了而且比 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 (
      
        
        
    {filteredList.map(item =>
  • {item.text}
  • )}
) } }

这要简单多了,而且和 derived state 版本一样好。

当使用memoization的时候,需要满足一些条件:

在大多数情况下,你会把内存化函数添加到一个组件实例上。这会防止该组件的多个实例重置每一个内存化属性。

通常你使用一个带有有限缓存大小的内存化工具,为的是防止时间累计下来的内存泄露。(在上述例子中,我们使用memoize-one因为它仅仅会缓存最近的参数和结果)。

这一节里,如果每次父组件渲染的时候props.list重新生成的话,上述实现会失效。但是在多数情况下,上述实现是合适的。

结束语

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96346.html

相关文章

  • You-Dont-Need : 你不需要系列

    摘要:是强大的,你可以做很多事情没有。如果你想要你的项目需要更少的依赖,并且你清楚的知道你的目标浏览器,那么你可能不需要。我们并不需要为了操作等再学习一下的。但是,他们往往需要更多的资源,功能不强,难以通过脚本自动化。 1 You-Dont-Need-JavaScript CSS是强大的,你可以做很多事情没有JS。 本文教你使用原生CSS做下面的事情。 内容目录 手风琴/切换 圆盘传送带...

    anonymoussf 评论0 收藏0
  • You-Dont-Need : 你不需要系列

    摘要:是强大的,你可以做很多事情没有。如果你想要你的项目需要更少的依赖,并且你清楚的知道你的目标浏览器,那么你可能不需要。我们并不需要为了操作等再学习一下的。但是,他们往往需要更多的资源,功能不强,难以通过脚本自动化。 1 You-Dont-Need-JavaScript CSS是强大的,你可以做很多事情没有JS。 本文教你使用原生CSS做下面的事情。 内容目录 手风琴/切换 圆盘传送带...

    bawn 评论0 收藏0
  • Memoization in JavaScript

    摘要:源码函数调用过,没有变化,参数时返回缓存值。而通过,可以把上一次的计算结果保存下来,而避免重复计算。这意味着将跳过渲染组件,并重用最后渲染的结果。 1. 基本概念 在一个CPU密集型应用中,我们可以使用Memoization来进行优化,其主要用于通过存储昂贵的函数调用的结果来加速程序,并在再次发生相同的输入时返回缓存的结果。例如一个简单的求平方根的函数: const sqrt = Ma...

    ccj659 评论0 收藏0
  • 前端小报 - 201903月刊

    摘要:热门文章我在淘宝做前端的这三年红了樱桃,绿了芭蕉。文章将在淘宝的三年时光折射为入职职业规划招聘晋升离职等与我们息息相关的经验分享,值得品读。 showImg(https://segmentfault.com/img/remote/1460000018739018?w=1790&h=886); 【Alibaba-TXD 前端小报】- 热门前端技术快报,聚焦业界新视界;不知不觉 2019 ...

    李义 评论0 收藏0
  • 前端小报 - 201903月刊

    摘要:热门文章我在淘宝做前端的这三年红了樱桃,绿了芭蕉。文章将在淘宝的三年时光折射为入职职业规划招聘晋升离职等与我们息息相关的经验分享,值得品读。 showImg(https://segmentfault.com/img/remote/1460000018739018?w=1790&h=886); 【Alibaba-TXD 前端小报】- 热门前端技术快报,聚焦业界新视界;不知不觉 2019 ...

    zhoutao 评论0 收藏0

发表评论

0条评论

URLOS

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<