一、派生state常见使用问题
大部分使用派生 state 导致的问题,不外乎两个原因:1,直接复制 props 到 state 上;2,如果 props 和 state 不一致就更新 state
直接复制props到state
最常见的误解就是 getDerivedStateFromProps
和 componentWillReceiveProps
只会在 props “改变”时才会调用。实际上只要父级重新渲染时,这两个生命周期函数就会重新调用,不管 props 有没有“变化”。所以,在这两个方法内直接复制(_unconditionally_)props 到 state 是不安全的。这样做会导致 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) {
// Do not do this!
if (nextProps.email !== this.state.email) {
this.setState({ email: nextProps.email });
}
}
}
class Timer extends Component {
state = {
count: 0
};
componentDidMount() {
this.interval = setInterval(
() =>
this.setState(prevState => ({
count: prevState.count + 1
})),
1000
);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return (
<Fragment>
<blockquote>请输入邮箱:</blockquote>
<EmailInput email="[email protected]" />
<p>
此组件每秒会重新渲染一次
</p>
</Fragment>
);
}
}
render(<Timer />, document.getElementById("root"));
state 的初始值是 props 传来的,当在 <input>
里输入时,修改 state。但是如果父组件重新渲染,我们输入的所有东西都会丢失,即使在重置 state 前比较 nextProps.email !== this.state.email
仍然会导致更新。
这个小例子中,使用 shouldComponentUpdate
,比较 props 的 email 是不是修改再决定要不要重新渲染。但是在实践中,一个组件会接收多个 prop,任何一个 prop 的改变都会导致重新渲染和不正确的状态重置。加上行内函数和对象 prop,创建一个完全可靠的 shouldComponentUpdate
会变得越来越难。而且 shouldComponentUpdate
的最佳实践是用于性能提升,而不是改正不合适的派生 state
在 props 变化后修改 state
继续上面的示例,我们可以只使用 props.email
来更新组件,这样能防止修改 state 导致的 bug
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) {
// Do not do this!
if (nextProps.email !== this.props.email) {
this.setState({ email: nextProps.email });
}
}
}
现在组件只会在 prop 改变时才会改变,但是仍然有个问题。想象一下,如果这是一个密码输入组件,拥有同样 email 的两个账户进行切换时,这个输入框不会重置(用来让用户重新登录)。因为父组件传来的 prop 值没有变化!
幸运的是,有两个方案能解决这些问题。这两者的关键在于,任何数据,都要保证只有一个数据来源,而且避免直接复制它。我们来看看这两个方案。
一、完全可控组件
我们都知道React表单中有受控组件,那么什么是完全可控组件呢,看下列示例你就明白了
function ControlledEmailInput(props) {
return (
<label>
Email: <input value={props.email} onChange={props.handleChange} />
</label>
);
}
class App extends Component {
state = {
draftEmail: '[email protected]'
};
handleEmailChange = event => {
this.setState({ draftEmail: event.target.value });
};
resetForm = () => {
this.setState({
draftEmail: '[email protected]'
});
};
render() {
return (
<Fragment>
<h1>此示例展示了什么是完全可控组件</h1>
<ControlledEmailInput
email={this.state.draftEmail}
handleChange={this.handleEmailChange}
/>
<button onClick={this.resetForm}>重置</button>
</Fragment>
);
}
}
render(<App />, document.getElementById("root"));
从上示例我们就知道了,这不正是我们所见过的组件间通信嘛,子组件中的email完全受父组件数据控制就像提线木偶一样
二、有key的完全不受控组件
让子组件自己存储临时的state数据,子组件仍然可以从 prop 接收“初始值”,但是更改之后的值就和 prop 没关系了
class EmailInput extends Component {
state = { email: this.props.defaultEmail };
handleChange = event => {
this.setState({ email: event.target.value });
};
resetForm = () => {
this.setState({ email: '[email protected]' });
};
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
}
class App extends Component {
inputRef = React.createRef();
state = {
draftEmail: '[email protected]',
};
resetForm = () => {
this.inputRef.current.resetForm()
};
render() {
return (
<Fragment>
<h1>此示例展示了什么是有Key的非可控组件</h1>
<EmailInput
defaultEmail={this.state.draftEmail}
ref={this.inputRef}
/>
<button onClick={this.resetForm}>重置</button>
</Fragment>
);
}
}
render(<App />, document.getElementById("root"));
总结
设计组件时,重要的是确定组件是受控组件还是非受控组件。
不要直接复制 props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。
对于不受控的组件,当你想在 prop 变化时重置 state 的话,可以选择一下几种方式:
- 建议: 重置内部所有的初始 state,使用
key
属性 - 选项一:仅更改某些字段,观察特殊属性的变化(比如
props.userID
)。 - 选项二:使用 ref 调用实例方法。