2

一、什么是派生state

简单来说,如果一个组件的state中的某个数据来自外部,就将该数据称之为派生状态。

大部分使用派生state导致的问题,不外乎两个原因:

  • 直接复制props到state
  • 如果props和state不一致就更新state

二、开发项目过程中遇到的派生state的问题

在我开发管理后台的需求的时候,由于借鉴了一下大象后台的部分代码,所以索性也就浏览了一下大象后台的代码。这在其中,我发现了一个派生状态的问题,引发了我的思考,下面是项目中的代码:
image.png

在这里,我发现了这样子的写法有三个不好的问题

  • 直接将props复制给了state,如果不采用生命周期钩子的话,这里会产生一个问题:当props更新的时候state并没有更新;

看下面的这一段代码:

  • 父组件
class Huang extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        count: 0
      }
    }

    componentDidMount() {
      this.interval = setInterval(() => {
        this.setState(preState => ({count: preState.count + 1}))
      }, 1000)
    }

    componentWillUnmount() {
      clearInterval(this.interval);
    }

    render() {
      return (
        <div>
          <p>{'父组件的值:' + this.state.count}</p>
          <Dong count={this.state.count}/>
        </div>
      )
    }
  }
  • 子组件
class Dong extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        count: this.props.state        //子组件的state是派生的
      }
    }

    // componentWillReceiveProps(nextProps) {
    //   if(this.state.count !== nextProps.count) {
    //     this.setState({count: nextProps.count});
    //   }
    // }
  
    render() {
      return (
        <div>
          <p>{'子组件的值:' + this.state.count}</p>
        </div>
      )
    }
  }

很明显,输出的结果就是:父组件的state在不断变化,但是子组件的state不会随着父组件的state改变而改变,也就印证了上面的结论。
image.png

  • 显然,作者也是意识到了这个问题,所以他下面使用了componentWillReceiveProps()钩子函数,想着当props更新时触发这个函数,在该函数中手动的使用了setState()方法修改了state。这样看似没有什么问题,但是这又引起了另外的一个问题:如果我想以另外的方式去修改state呢?比如说在<input>里监听输入,修改state。但是如果父组件重新渲染,我们输入的所有东西都会丢失!关于这个问题可以看一下例子:无论在输入框中输入什么每一秒都会自动重制输入框的内容
  • 退一步,即使你只是在componentWillReceiveProps()会去使用setState()去修改子组件中state的值。是不是这样就没有问题了呢?但是,还是会有一个问题:试想,子组件的state的某个字段是完全依赖于props的,也就是说只要props不更新,state就不会去更新,这样子组件就不会重新渲染我觉得官网举的这个例子很好:想象一下,如果这是一个密码输入组件,拥有同样 email 的两个账户进行切换时,这个输入框不会重置(用来让用户重新登录)。因为父组件传来的 prop 值没有变化!这会让用户非常惊讶,因为这看起来像是帮助一个用户分享了另外一个用户的密码。这个是官网关于这个问题的一个例子当切换选项时由于子组件的props没变,所以子组件不会被重新渲染

三、如何解决派生state的问题

在这里,我稍微总结了几种方法,仅供大家参考。

1.干脆就不用state,直接将组件变为完全可控组件

阻止上述问题发生的一个方法是:从组件里删除state,然后将组件变为一个完全受控的组件,这样就不会产生派生状态的影响。像下面这样:

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

2.有key的非可控组件

有很多时候会有这样的一种情况,组件的state里面不完全是派生组件,组件也不完全是受控组件,这时候如果我们想用派生状态的话怎么办?

为每一个组件设置一个key属性,这样可以保证当key发生改变的时候组件被重置,也就是重新创建一个新的组件。

  • 子组件
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} />;
  }
}
  • 父组件中使用子组件
<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}        //这个id必须是唯一的,当id发生变化的时候,会创建一个新的子组件
/>

在这里要注意,不能使用this.props.key来读取key的值

3.用prop的ID重置非受控组件

如果某些情况下 key 不起作用(可能是组件初始化的开销太大),一个麻烦但是可行的方案是在 getDerivedStateFromProps 观察 userID 的变化:

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

  static getDerivedStateFromProps(props, state) {
    // 只要当前 user 变化,
    // 重置所有跟 user 相关的状态。
    // 这个例子中,只有 email 和 user 相关。
    if (props.userID !== state.prevPropsUserID) {
      return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }

  // ...
}

注意:上面的示例使用了 getDerivedStateFromProps,用 componentWillReceiveProps 也一样。

4.使用实例方法重置非受控组件

更少见的情况是,即使没有合适的 key,我们也想重新创建组件。一种解决方案是给一个随机值或者递增的值当作 key,另外一种是用实例方法强制重置内部状态

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

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

  // ...
}

然后父组件可以使用ref调用这个方法

refs 在某些情况下很有用,比如这个。但通常我们建议谨慎使用。即使是做一个演示,这个命令式的方法也是非理想的,因为这会导致两次而不是一次渲染。

四、总结

上面的派生状态引发的问题提醒我们:设计组件时,重要的是确定组件是受控组件还是非受控组件。

不要直接复制(mirror) props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。比如,不要在子组件里被动的接受 props.value 并跟踪一个临时的 state.value,而要在父组件里管理 state.draftValue 和 state.committedValue,直接控制子组件里的值。这样数据才更加明确可预测。

对于不受控的组件,当你想在 prop 变化(通常是 ID )时重置 state 的话,可以选择一下几种方式:

  • 建议: 重置内部所有的初始 state,使用 key 属性
  • 选项一:仅更改某些字段,观察特殊属性的变化(比如 props.userID)。
  • 选项二:使用 ref 调用实例方法。

这篇文章只是出于我个人遇到派生state引发的问题的一些思考,如果大家对这个问题比较感兴趣,可以参考官网这篇文章:你可能不需要使用派生state


JacksonHuang
74 声望3 粉丝