getDerivedStateFromProps有什么用?
React生命周期的命名一直都是非常语义化的,这个生命周期的意思就是从props中获取派生state,意思非常明确。
可以说,这个生命周期的功能实际上就是将传入的props映射到state上面,基本上就是直接替换了原来的componentWillReceiveProps。
如何使用?
getDerivedStateFromProps接受两个参数props和state,根据变化来返回最新的state,如果不变化就返回null。
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
const { number } = this.state;
return (
<div>
<Child number={number} />
<button onClick={this.handleClick}>add one</button>
</div>
);
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
number: props.number,
};
}
static getDerivedStateFromProps(props, state) {
if (props.number !== state.number) {
return { number: props.number };
}
// 没有变化就返回null
return null;
}
render() {
const { number } = this.state;
return <div>number is: {number}</div>;
}
}
和componentWillReceiveProps有什么区别?
但是在16.4版本之后,事情变得有意思了(16.4之前的版本setState并不会进入getDerivedStateFromProps),这个函数会在每次re-rendering之前被调用,这意味着什么呢?
意味着即使你的props没有任何变化,而是本身的state发生了变化,导致子组件发生了re-render,这个生命周期函数依然会被调用。看似一个非常小的修改,却可能会导致很多隐含的问题。
export default class Demo1 extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps');
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
console.log('render');
const { number } = this.state;
return (
<div>
<div>{number}</div>
<button onClick={this.handleClick}>add one</button>
</div>
);
}
}
这个例子中一开始就会打印一次getDerivedStateFromProps,每点击一次button,都会先打印getDerivedStateFromProps,然后打印render。说明getDerivedStateFromProps这个生命周期已经和我们对componentWillReceiveProps的理解不一样了,只要有重新渲染都会进来(包括第一次渲染的时候)。
坑从这里开始。
采坑:setState不会引发重新渲染
由于setState也会进入getDerivedStateFromProps,所以上面的代码会导致setState时,组件不会重新渲染。
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
const { number } = this.state;
return (
<div>
<Child number={number} />
<button onClick={this.handleClick}>add one(outer)</button>
</div>
);
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
number: props.number,
};
}
static getDerivedStateFromProps(props, state) {
if (props.number !== state.number) {
// 即使是setState引发的变化这里return的也是props,所以setState不会引发组件重新渲染
return { number: props.number };
}
return null;
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
const { number } = this.state;
return (
<div>
<div>number is: {number}</div>
{/* 这个按钮点击无效 */}
<button onClick={this.handleClick}>add one(inner)</button>
</div>
);
}
}
填坑:只有当props变化时,才重新返回state
// getDerivedStateFromProps和componentWillReceiveProps的区别
// getDerivedStateFromProps与setState并用出现的问题
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
const { number } = this.state;
return (
<div>
<Child number={number} />
<button onClick={this.handleClick}>add one(outer)</button>
</div>
);
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
number: props.number,
};
}
static getDerivedStateFromProps(props, state) {
// 只有props引发的变化才会进入这里
if (props.number !== state.prevNumber) {
return {
number: props.number,
prevNumber: props.number,
};
}
console.log('change from setState');
return null;
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
const { number } = this.state;
return (
<div>
<div>number is: {number}</div>
<button onClick={this.handleClick}>add one(inner)</button>
</div>
);
}
}
通过保存一个之前 的prop 值,我们就可以在只有 prop 变化时才去修改 state。这样就解决上述的问题。
出现的另外一个问题是,当我们通过Child组件的setState改变number之后,再通过父组件的props改变,number会被重置,这问题其实就是由于数据源的多样性所导致的,父组件和子组件都在控制这个状态,而两边的状态是不一致的。
注意:这里有一个比较容易令人迷惑的点,当prop引发的变化进入该生命周期时,如果return null,表示不进行重新渲染,当state引发的变化进入该生命周期时,如果return null,表示按照既有state重新渲染。其实归根到底就是将return的state与之前的state进行合并后,再交由componentShouldUpdate进行对比,决定是否要重新渲染。
到底该不该用getDerivedStateFromProps
大多数情况下,我们是不应该使用getDerivedStateFromProps的,我们总能找到更加更加合适且可靠的方式去维护状态。
场景一:prop和state同时控制一个状态改变
正如上述案例描述的那样,当数据源同时来自prop和state时,会产生数据混乱的问题,此时,我们应当尽可能将数据交由父组件管理,完全由prop来改变状态,这就是所谓的完全受控组件。
<Child number={number} onChange={this.handleClick} />
场景二:prop未发生变化,但是state需要重置
除了上述,数据源多样性所导致的问题之外,还有一个问题,组件只会在 prop 改变时才会改变。想象一下,如果有一个密码输入组件,拥有同样 email 的两个账户进行切换时,这个输入框不会重置(用来让用户重新登录)。因为父组件传来的 prop 值没有变化!这会让用户非常惊讶,因为这看起来像是帮助一个用户分享了另外一个用户的密码,(查看这个示例)。
我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。 Keys 一般用来渲染动态列表,但是这里也可以使用。当用户输入时,我们使用 user ID 当作 key 重新创建一个新的 email input 组件:
<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>
每次 ID 更改,都会重新创建 EmailInput ,并将其状态重置为最新的 defaultEmail 值。(点击查看这个模式的演示) 使用此方法,不用为每次输入都添加 key,在整个表单上添加 key 更有位合理。每次 key 变化,表单里的所有组件都会用新的初始值重新创建。
大部分情况下,这是处理重置 state 的最好的办法。
总结
在实际应用中,如果每个值都有明确的来源,就可以避免上面提到的反面模式,用单一源来控制会使得数据可靠得多。getDerivedStateFromProps是一个高级复杂的功能,应该保守使用。