美文网首页
你也许不需要派生状态(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