【译】你可能不须要派生状态

原文连接: https://reactjs.org/blog/2018...

翻译这篇文章的原由是由于在一次需求迭代中错误的使用了getDerivedStateFromProps这个生命周期致使子组件的state被循环重置,因而翻到了这篇文章,而后就开启的翻译之旅。javascript


在很长一段时间,生命周期componentWillReceiveProps是用来响应props更新来改变state并不须要额外渲染的惟一方法。在16.3版本中,咱们提供了getDerivedStateFromProps这个更安全生命周期来解决相同的用例。同时,咱们发现人们对于如何使用这两种方式有不少误解,而且咱们发现了一些形成微妙和使人混淆的反模式。在16.4中的getDerivedStateFromProps的bug修复使得派生状态更加可预测,且更容易让人注意到错误使用它的结果。html

何时去使用派生状态

getDerivedStateFromProps的存在只有一个目的。它可使组件根据props的改变来更新内部的state。咱们之间的博客提供了一些例子:经过改变offset的prop来改变当前的滚动方向加载经过source props所指定的外部数据java

咱们没有提供更多的例子,由于做为一个基本的规则,派生状态应该被谨慎的使用。全部派生状态致使的问题无异于两种:(1)无条件的根据props来更新state(2)不管props和state是否匹配来更新state。react

  • 若是仅用派生状态来记录一些基于当前props的计算,则不须要派生状态;
  • 若是你无条件的更新派生状态,或者不管props和state是否匹配来更新state,你的组件将会过于频繁的去重置状态;

使用派生状态的常见问题

“受控的”和“不受控的”一般用来指表单的输入,但它也一样能够表示任何组件数据所在的位置。数据经过props传来被认为是“受控的”(由于父组件在控制着这个数据)。数据仅存在其内部的state中被认为是“不受控的”(由于其父组件不能直接的改变这它)。缓存

派生状态最多见的错误就是将这二者混和在一块儿。当一个派生状态的值一样经过setState的调用来更新时,这就没法保证数据有单一的真实来源。这也许和上面提到的外部数据加载的例子很类似,但他们在一些重要的方面上是不一样的。在加载的例子中,”source“的props和”loading“的state都有一个明确的真实来源。当source props改变的时候,应该老是覆盖loading state。相反,只有props改变且由组件管理的时候,才去重写state。安全

当这些约束中的任何一个被改变时将会出现问题。一般有两种形式,让咱们接下来看一下这两种形式。性能优化

反模式:无条件的从prop复制状态到state

一个常见的误解是getDerivedStateFromPropscomponentWillReceiveProps只有在props改变的时候会被调用。这两个生命周期将会在父组件从新渲染的任什么时候间被调用,而无论props是否与以前不一样。所以,在使用这两个生命周期时,无条件的覆盖state老是不安全的,将会致使state更新时的丢失app

让咱们考虑一个例子来讲明这个问题。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) {
    // 这里将会覆盖任何本地state的更新。
    this.setState({ email: nextProps.email });
  }
}

这个组件看起来可能没有问题,state由prop传来的数据初始化,且当咱们改变input的值时state被更新。可是当咱们的父组件从新渲染的时候,任何咱们在input中输入的状态都将丢失,即便咱们去比较nextProps.email !== this.state.email也是如此。函数

在这个例子中,只有当email的prop的改变的时候添加shouldComponentUpdate来从新渲染能够解决这个问题,可是实际上,组件一般会接收多个prop,另外一个prop的改变任然会形成组件的从新渲染和不正确的重置。此外函数和对象的prop一般也会是内联建立的,这也会使shouldComponentUpdate正确的返回true变得困难。这里有一个例子。所以shouldComponentUpdate一般被用于性能优化而不是来判断派生状态的正确性。

但愿到如今你们清楚为何不要无条件的复制props到state。在咱们找到可能的解决方案以前,让咱们去看一个与之相关的问题:若是只在props.email改变的时候去更新state会怎样?

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

继续上面的例子,咱们能够避免在props.email更改时意外的清除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
      });
    }
  }
  
  // ...
}

咱们取得了很大的进步,如今咱们的组件只有在props真正改变的时候才会清除state。

还有一个微妙的问题,想象一下使用以上的组件来构建密码管理应用。当使用同一个email在两个帐户的详情页导航时,input将会没法重置,这是由于传递给组件的props相对于两个帐号来讲时相同的。这对用户来讲将会是一个惊喜,由于对一个帐户的未保存更改会错误的影响到另外一个帐户。查看演示

这种设计从本质上来讲是错误的,但倒是一个很容易犯的错误,幸运的是,有两种更好的选择,这二者的关键在于,对于任何数据片断,你都须要选择一个将它做为数据源的组件,而避免在其它组件重复使用。

首选方案

推荐:彻底受控组件

避免上述问题的一个方案是彻底移除组建中的state,若是email仅做为props存在,那咱们将没必要担忧它和state冲突,咱们甚至能够讲EmailInput组件变为更轻量级的function组件:

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

这种方法简化了组件的实现,但若是你仍须要存储一个草稿值,那么父表单组件如今须要手动执行该操做。查看演示

推荐:带有key的彻底不受控组件

另外一个方法是让咱们的组件彻底拥有“草稿”email的state,这时咱们的组件仍然能够接收props来做为初始值,可是它会忽略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一般被用在动态的list可是一样能够在这里使用。

<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

每当id改变的时候,EmailInput组件将会被从新建立,它的state将会被重置为最后一次的defaultEmail的值。查看演示。使用此方法,你讲不用在每个input上添加key,把一个key放在整个form上更有意义,每当key改变的时候,表单中的input都会重置到其初始状态。

替代方案1:经过ID prop来重置不受控组件

若是key在某些场合不适用(也许初始化对于组件来讲是昂贵的),一个可行但繁琐的方式是在getDerivedStateFromProps中去监测userID:

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };

  static getDerivedStateFromProps(props, state) {
    if (props.userID !== state.prevPropsUserID) {
      return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }

  // ...
}

这也提供了灵活性,若是咱们选择,只重置组件内的部分state。查看演示

替代方案2:经过实例方法来重置不受控组件

若是没有合适的id来做为key可是又要重置状态,一种解决方案是为组件生成一个随机数或者自动递增值来做为key,另外一种方案是经过实例的方法来强制重置组件的state。

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

  resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
  }

  // ...
}

父组件将经过ref拿到组件的实例从而调用该方法。查看演示

在某些场景下ref会颇有用,可是咱们建议你谨慎的使用它,即便在demo中,这个方法也是最不理想的,由于将会形成两次渲染而不是一个。

总结

总而言之,当设计一个组件时,一个重要的方面是它的数据是可控的仍是不可控的。

尽可能避免在state中去“镜像”一个props值,使这个组件成为受控组件,在父组件的state中去合并这两个state。例如,与其在组件中去接受一个committed的props而且跟踪一个draft的state,不如让父组件去同时管理这个state.draftValue和state.committedValue并直接控制子组件,这将使组件更加的明确和可预测。

对于一个不受控组件,若是你想根据一个props的改变来重置state,你须要遵循如下几点:

  • 首选:要重置所有内部state,使用key属性;
  • 备选1:若是只重置部分state,监测props中属性的变化;
  • 备选2:还能够考虑经过ref调用实力的方法;

memoization怎样?

咱们还看到了派生状态用于确保渲染中使用的昂贵值仅在输入发生变化时才会从新计算,这种技术叫作memoization

使用派生状态来作memoization不必定是坏事,但一般不是最好的解决办法。派生状态的管理存在必定的复杂性,而且这种复杂性随着属性的增长而增长。例如,若是咱们向组件的state添加第二个派生字段,那么咱们的实现将须要分别跟踪对两个字段的更改。

让咱们看一个组件的示例,该组件使用一个prop(项目列表)并呈现与用户输入的搜索查询匹配的项。咱们可使用派生状态来存储过滤列表:

class Example extends Component {
  state = {
    filterText: "",
  };

  // *******************************************************
  // NOTE: this example is NOT the recommended approach.
  // See the examples below for our recommendations instead.
  // *******************************************************

  static getDerivedStateFromProps(props, state) {
    // Re-run the filter whenever the list array or filter text change.
    // Note we need to store prevPropsList and prevFilterText to detect changes.
    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并将filter操做放到render中来简化操做:

// PureComponents只有在至少一个state或者prop改变的时候才会从新渲染
// 经过对state和props的keys的浅比较来确认改变。
class Example extends PureComponent {
  state = {
    filterText: ""
  };

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    // 只有props.list 或 state.filterText 改变的时候PureComponent的render才会调用
    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>
    );
  }
}

上述例子比派生状态的版本更加的干净和简洁,可是有些时候这可能还不够好,例如对于大型列表来讲,过滤可能很慢,且若是有其余的props改变PureComponent也不会阻止其从新渲染。为了解决这两个问题,咱们能够添加一个memoization,以免没必要要地从新过滤咱们的列表:

import memoize from "memoize-one";

class Example extends Component {
  state = { filterText: "" };

  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    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>
    );
  }
}

当使用memoization时,有如下约束:

  • 在大多数状况下,您须要将memoized函数附加到组件实例。这能够防止组件的多个实例重置彼此的memoized key。
  • 一般状况下,您须要使用具备有限缓存大小的memoization,以防止内存泄漏。(在上面的例子中,咱们使用了memoize-one,由于它只缓存最近的参数和结果。)
  • 若是每次父组件呈现时从新建立props.list,本节中显示的实现都不会起做用。但在大多数状况下,这种设置是合适的。

最后

在实际应用中,组件一般包含受控和不受控制行为混合。不要紧,若是每一个值都有明确的来源,则能够避免上面提到的反模式。

值得从新思考的是,getDerivedStateFromProps(以及一般的派生状态)是一种高级功能,应该谨慎使用。

相关文章
相关标签/搜索