一、什么是派生state
简略来说,如果一个组件的state中的某个数据来自内部,就将该数据称之为派生状态。
大部分应用派生state导致的问题,不外乎两个起因:
- 间接复制props到state
- 如果props和state不统一就更新state
二、开发我的项目过程中遇到的派生state的问题
在我开发治理后盾的需要的时候,因为借鉴了一下大象后盾的局部代码,所以索性也就浏览了一下大象后盾的代码。这在其中,我发现了一个派生状态的问题,引发了我的思考,上面是我的项目中的代码:
在这里,我发现了这样子的写法有三个不好的问题:
- 间接将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扭转而扭转,也就印证了下面的论断。
- 显然,作者也是意识到了这个问题,所以他上面应用了
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