美文网首页
你也许不需要派生状态(You Probably Don't Ne

你也许不需要派生状态(You Probably Don't Ne

作者: 木匠_说说而已 | 来源:发表于2019-01-26 16:57 被阅读0次

原文地址
June 07, 2018 by Brian Vaughn

(我翻译时候使用的React和ReactDOM版本是16.7)

React 16.4修复了一个getDerivedStateFromProps的bug,使得一些现有的React组件bug可以一致地复现(复制?)。如果这次发布使你的反模式(anti-pattern)应用不能工作,很抱歉。在这篇文章中,我们将解释一些关于derived state的常用反模式以及我们推荐的替代方案。

很久以来,componentWillReceiveProps生命周期是在props发生改变时候更新state,并且不会额外引起render的唯一方案。在16.3版本,我们介绍了一个替代的生命周期getDerivedStateFromProps,解决同样的问题,更安全。同时,我们发现大家对这两个方法有很多错误的认识和用法,并且我们发现反模式导致了微妙的、令人困惑的bugs。这次16.4关于getDerivedStateFromProps的修复,让derived state更加可预见,所以更容易注意到滥用的结果。

Note
本文所有关于反模式的描述,对旧的componentWillReceiveProps和新的getDerivedStateFromProps都同样适用

本文将涵盖以下话题:

  • 什么时候使用derived state
  • 使用derived state时候的常见bugs
    • 反模式:无条件复制props到state
    • 反模式:props改变的时候改变state
  • 优先的解决方案
  • 什么是记忆?

什么时候使用Derived State

getDerivedStateFromProps的存在只为了一个目的。允许组件根据props变化更新内部state。我们的前一篇博客提供了一些例子,像根据offset prop记录当前的滚动位置,像根据source prop加载外部数据。

我们不提供很多例子,因为根据一般规则,derived state应该尽少使用。目前所有使用derived state引起的问题我们可以归纳为以下

  1. 无条件地通过props更新state
  2. 不管props和state是否匹配,都更新state

(下面会说更多细节)

  • 如果你使用derived state来记录一些通过当前props计算出来的值,你不需要使用derived state
  • 如果你更新derived state,无条件地,或者不管props和state匹配不,都更新,那你的组件改变state太频繁了。细节如下。

使用Derived State的常见bugs

术语“controlled”和“uncontrolled”通常是指表单字段,但是也可以描述为组件的数据在哪里。通过props进来的数据可以认为是controlled(因为它的父组件控制了数据)。只存在于内部state的数据可以认为是uncontrolled(因为它的父组件不能直接改变这个数据)。

最常见的derived state错误使用是混合这两种。当一个derived state值,也可以被setState改变,那么这个数据没有唯一的来源。上面提到的external data loading example 示例也许看起来很像,但是其中有几个关键点不同。在这个加载示例中,“source” prop和“loading” state都有清晰的来源。当source prop发生改变,loading state应该始终被覆盖。反过来说,state应该只有当props改变和被组件其他情况改变,才能被覆盖。

当这些约束中的任何一个发生改变时,就会出现问题。这通常有两种形式,让我们来看看。

反模式:无条件复制props到state

一个常见的误解是getDerivedStateFromProps和componentWillReceiveProps都只有在props改变时候才调用。这俩生命周期在父组件rerenders的时候始终被调用,不管props有没有发生改变。所以,在这俩生命周期中无条件地改变state是不安全的。这么做会引起state更新丢失

我们思考一个例子来证明这个问题。这儿有一个EmailInput组件,其prop .email有一个“镜像”在state中。

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 will erase any local state updates!
    // Do not do this.
    this.setState({ email: nextProps.email });
  }
}

首先,这个组件看起来挺好,state value值特殊初始化为props中值,并且当手动输入<input> 时候更新。但是当父组件renders,所有我们手动输入当值会丢失!(看这个例子)。即使我们在重置前比较了 nextProps.email !== this.state.email,也会这样。(老实讲我没试出来作者这句话表达的意思)

可以通过这样修复问题:在shouldComponentUpdate中添加控制只有 prop.email改变才rerender。然而在实践中,组件通常接受组合props;其他prop改变仍然会引起rerender并且不恰当地重置。Function和对象props经常内联地创建,导致通过shouldComponentUpdate去控制只有当实质变化发生才返回true变得很困难。(这儿有个例子)。所以,shouldComponentUpdate最好用来做性能优化,而不是去确定derived state的正确性(即是否更新state)。

希望大家清楚了为什么无条件复制props到state是不好的。在查看解决方案前,我们来看一个相对有问题的模式:只有当email prop改变的时候才更新state。

反模式:当props改变时候改变state

继续上面的例子,我们可以通过只有props.email改变时候才更新state,来避免意外地更新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
      });
    }
  }
  
  // ...
}

Note
即使上面例子用的是componentWillReceiveProps,同样的反模式也适用于getDerivedStateFromProps

我们已经做了一个很大的改进。现在我们的组件只会在props确实改变的时候才覆盖更新我们的手输值。

但是仍然有个微妙的问题。想象以下,一个密码管理软件使用上面的组件。当两个账户信息切换,但是使用同一个邮箱时候,输入框重置失败。这是因为对于两个账户来说,组件接收的props值是一样的。这对用户来说是一个惊喜,一个账户未保存的邮箱会影响另外一个账户显示同样的(看例子)。

这个设计本质上是有缺陷的,但是也很容易犯。(我自己也犯过!)幸运的是有两种替代方案更好。两种方案的关键是,对于一个数据块,你需要选择一个组件拥有它作为真实来源,并且避免在另外一个组件复制它。我们来看一下每个方案。


优先方案

建议:完全受控组件(Fully controlled component)

避免上面问题的一种方式是直接移除state。如果邮箱只作为prop存在,那么我们不用担心和state冲突。我们甚至可以把EmailInput改为轻量的函数式组件:

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

这种方式简化了组件的实现,如果仍然想存储一些值,那么需要手动去父组件完成(点击查看这种模式例子)。

建议:带key的完全非受控组件(Fully uncontrolled component with a key)

另外一个替代方案是组件完全拥有email state。组件仍然接受一个prop(email)作为初始值,但是会忽略后来的prop变化。

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会创建一个新的组件而不是更新现有的。Keys经常在动态列表中使用,但是这种情形也适用。在我们的例子中,我们可以使用用户ID来控制选择用户时候,重新创建组件。

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

每当ID改变,EmailInput会被重建,同时它的email state会被重置为defaultEmail prop值。(点击查看)。这种情况,你不需要为每个表单元添加==key==。通常情况下是为整个表单设置一个key,每当key改变,整个表单内组件会重建,state会被初始化为新值。

大多数情况下,这是处理state需要重置的最好方式。

Note
这种方式听起来会慢,性能差别通常是无关紧要的。使用key甚至会更快:如果组件更新时候,在diffing gets bypassed for that subtree之前有大量的逻辑运算.

替代方案1:

如果key因为某些情况不能使用(也许组件初始化非常耗资源),通过 ID prop重置非受控组件,一个可行的但是很繁琐的解决方式是在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;
  }

  // ...
}

我不知道是不是此博客写的时候 getDerivedStateFromProps 不会在组件初始化时候被调用,所以作者在state=中也初始化了。我用react16.7,初始化时候getDerivedStateFromProps也被调用,所以上面代码逻辑会有点重复,state={}即可,)

这也提供了一种灵活改变组件内部部分state的方式(查看例子

Note
即使上面使用的是getDerivedStateFromProps,同样也适用于componentWillReceiveProps。

替代方案2:通过实例方法重置非受控组件

更罕见的,你可能需要重置state,即使没有合适的ID作为key。一种解决是当你想重置的时候,设置一个随机数,或者自增数,作为key。另外一种可能的替代方案是写一个实例方法,通过调用重置state。

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

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

  // ...
}

父表单组件可以通过ref来调用这个方法。(查看事例

Refs在某些情况下很有用,譬如这个例子。但是建议你少用。即使这个例子中,这个必要的方法也是非理想的,因为会出现两次render。


概要

为了概括,我们设计一个组件,决定他的数据是受控还是非受控非常重要。

与其尝试将prop“镜像”到state,不如让组件受控,并且在父组件中合并这两个发散的值。例如,让父组件管理state.draftValue和 state.committedValue同时直接控制子组件value,而不是让子组件接收一个“committed” props.value 同时跟踪“draft” state.value。这样使数据流更清晰可预见。

对于非受控组件,如果你想在部分prop(通常是ID)改变时候重置state,有几种选择:

  • 建议:如果想重置所有内部state,使用key属性
  • 替代方案1:只重置特定state,监听一个特殊prop改变(例如 props.userID)
  • 替代方案2:也可以考虑通过refs回调一个实例方法

什么是记忆化?

我们还遇到在render中要使用的重要的值是derived state,只有在用户输入时候会被重新计算。这个技巧就是记忆化。

为了记忆化使用derived state不是绝对坏的,但是通常不是最好的解决方法。管理derived state非常复杂,并且这个复杂性随着属性增加而增加。例如,如果为组件增加第二个derived state,那么我们的代码实现得分别跟踪两个derived state。

我们看个例子,一个组件有一个prop:a list of items,并且根据用户输入renders相匹配的items。我们可以使用derived state存储filtered列表。

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变化来更新必要的filtered list。我们可以通过使用PureComponent和把过滤操作放到render中来简化。

// PureComponents only rerender if at least one state or prop value changes.
// Change is determined by doing a shallow comparison of state and prop keys.
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() {
    // The render method on this PureComponent is called only if
    // props.list or state.filterText has changed.
    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>
    );
  }
}

上面的方式比derived state版更简洁清晰。偶尔,这样不够好-大列表过滤可能慢,并且如果其他prop改变,PureComponent不会阻止renders。为了解决这俩关注点,可以添加一个memoization helper来避免非必要的重新过滤列表。

import memoize from "memoize-one";

class Example extends Component {
  // State only needs to hold the current filter text value:
  state = { filterText: "" };

  // Re-run the filter whenever the list array or filter text changes:
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

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

  render() {
    // Calculate the latest filtered list. If these arguments haven't changed
    // since the last render, `memoize-one` will reuse the last return value.
    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>
    );
  }
}

这个更简单,并且执行起来和derived state版差不多好。

当使用记忆化时候,记得几个约束条件:

  1. 大多数情况,你得把memoized function放到一个组件实例上。这样会防止多个组件实例重新设置彼此的记忆键
  2. 通常,您会希望使用缓存大小有限的memoization helper,以防止久了出现内存泄漏。(在上面的例子中,我们使用memoize- one,因为它只缓存最近的参数和结果。)
  3. 如果props.list在每次父组件renders时候都重建,本节的所有实现都不能用,但是大多数情况下是合适的。

结束语

在真实的应用中,组件经常包含受控和非受控行为。这没问题!如果每个值有清晰的来源,你可以避免上面提到的反模式。

值得强调的是 getDerivedStateFromProps(以及通常的derived state)是个高级特性并且应当少用因为它的复杂性。如果你的使用情况超出了这些模式,请在GitHub或者Twitter上分享给我们!

相关文章

网友评论

      本文标题:你也许不需要派生状态(You Probably Don't Ne

      本文链接:https://www.haomeiwen.com/subject/ehiijqtx.html