一、什么是派生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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。