一.对componentWillReceiveProps的误解
componentWillReceiveProps
通常被认为是propsWillChange
,我们确实也通过它来判断props change。但实际上,componentWillReceiveProps
在每次rerender
时都会调用,无论props
变了没:
class A extends React.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
componentWillReceiveProps(nextProps) {
console.log('Running A.componentWillReceiveProps()');
}
}
class B extends React.Component {
constructor() {
super();
this.state = { counter: 0 };
}
render() {
return <A name="World" />
}
componentDidMount() {
setInterval(() => {
this.setState({
counter: this.state.counter + 1
});
}, 1000)
}
}
ReactDOM.render(<B/>, document.getElementById('container'));
上例中,父组件B
的state change引发子组件A
的render
及componentWillReceiveProps
被调用了,但A
并没有发生props change
没错,只要接到了新的props
,componentWillReceiveProps
就会被调用,即便新props
与旧的完全一样:
UNSAFE_componentWillReceiveProps() is invoked before a mounted component receives new props.
Note that if a parent component causes your component to re-render, this method will be called even if props have not changed.
相关实现如下:
updateComponent: function () {
var willReceive = false;
var nextContext;
if (this._context !== nextUnmaskedContext) {
nextContext = this._processContext(nextUnmaskedContext);
willReceive = true;
}
// Not a simple state update but a props update
if (prevParentElement !== nextParentElement) {
willReceive = true;
}
if (willReceive && inst.componentWillReceiveProps) {
inst.componentWillReceiveProps(nextProps, nextContext);
}
}
(摘自典藏版ReactDOM v15.6.1)
也就是说,componentWillReceiveProps
的调用时机是:
引发当前组件更新 && (context发生变化 || 父组件render结果发生变化,即当前组件需要rerender)
注意,这里并没有对props
做diff
:
React doesn’t make an attempt to diff props for user-defined components so it doesn’t know whether you’ve changed them.
因为props
值没什么约束,难以diff
:
Oftentimes a prop is a complex object or function that’s hard or impossible to diff, so we call it always (and rerender always) when a parent component rerenders.
唯一能保证的是props change一定会触发componentWillReceiveProps
,但反之不然:
The only guarantee is that it will be called if props change.
P.S.更多相关讨论见Documentation for componentWillReceiveProps() is confusing
二.如何理解getDerivedStateFromProps
getDerivedStateFromProps
是用来替代componentWillReceiveProps
的,应对state
需要关联props
变化的场景:
getDerivedStateFromProps exists for only one purpose. It enables a component to update its internal state as the result of changes in props.
即允许props
变化引发state
变化(称之为derived state,即派生state),虽然多数时候并不需要把props
值往state
里塞,但在一些场景下是不可避免的,比如:
这些场景的特点是与props
变化有关,需要取新旧props
进行比较/计算,
与componentWillReceiveProps
类似,getDerivedStateFromProps
也不只是在props change时才触发,具体而言,其触发时机为:
With React 16.4.0 the expected behavior is for getDerivedStateFromProps to fire in all cases before shouldComponentUpdate.
更新流程中,在shouldComponentUpdate
之前调用。也就是说,只要走进更新流程(无论更新原因是props change还是state change),就会触发getDerivedStateFromProps
就具体实现而言,与计算nextContext
(nextContext = this._processContext(nextUnmaskedContext)
)类似,在确定是否需要更新(shouldComponentUpdate
)之前,要先计算nextState
:
export function applyDerivedStateFromProps(
workInProgress: Fiber,
ctor: any,
getDerivedStateFromProps: (props: any, state: any) => any,
nextProps: any,
) {
const prevState = workInProgress.memoizedState;
const partialState = getDerivedStateFromProps(nextProps, prevState);
// Merge the partial state and the previous state.
const memoizedState =
partialState === null || partialState === undefined
? prevState
: Object.assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
// Once the update queue is empty, persist the derived state onto the
// base state.
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
updateQueue.baseState = memoizedState;
}
}
(摘自react/packages/react-reconciler/src/ReactFiberClassComponent.js)
getDerivedStateFromProps
成了计算nextState
的必要环节:
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.
function mountIndeterminateComponent(
current,
workInProgress,
Component,
renderExpirationTime,
) {
workInProgress.tag = ClassComponent;
workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;
const getDerivedStateFromProps = Component.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
Component,
getDerivedStateFromProps,
props,
);
}
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress, Component, props, renderExpirationTime);
// 调用render,第一阶段结束
return finishClassComponent(
current,
workInProgress,
Component,
true,
hasContext,
renderExpirationTime,
);
}
(摘自react/packages/react-reconciler/src/ReactFiberBeginWork.js)
所以在首次渲染时也会调用,这是与componentWillReceiveProps
相比最大的区别
三.派生state实践原则
实现派生state有两种方式:
getDerivedStateFromProps
:从props
派生出部分state
,其返回值会被merge
到当前state
componentWillReceiveProps
:在该生命周期函数里setState
实际应用中,在两种常见场景中容易出问题(被称为anti-pattern,即反模式):
props
变化时无条件更新state
更新
state
中缓存的props
在componentWillReceiveProps
时无条件更新state
,会导致通过setState()
手动更新的state
被覆盖掉,从而出现非预期的状态丢失:
When the source prop changes, the loading state should always be overridden. Conversely, the state is overridden only when the prop changes and is otherwise managed by the component.
例如(仅以componentWillReceiveProps
为例,getDerivedStateFromProps
同理):
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) {
// This will erase any local state updates!
// Do not do this.
this.setState({ email: nextProps.email });
}
}
上例中,用户在input
控件中输入一串字符(相当于手动更新state
),如果此时父组件更新引发该组件rerender
了,用户输入的内容就被nextProps.email
覆盖掉了(见在线Demo),出现状态丢失
针对这个问题,我们一般会这样解决:
class EmailInput extends Component {
state = {
email: this.props.email
};
componentWillReceiveProps(nextProps) {
// Any time props.email changes, update state.
if (nextProps.email !== this.props.email) {
this.setState({
email: nextProps.email
});
}
}
}
精确限定props change到email
,不再无条件重置state
。似乎完美了,真的吗?
其实还存在一个尴尬的问题,有些时候需要从外部重置state
(比如重置密码输入),而限定state
重置条件之后,来自父组件的props.email
更新不再无条件传递到input
控件。所以,之前可以利用引发EmailInput
组件rerender
把输入内容重置为props.email
,现在就不灵了
那么,需要想办法从外部把输入内容重置回props.email
,有很多种方式:
EmailInput
提供resetValue()
方法,外部通过ref
调用外部改变
EmailInput
的key
,强制重新创建一个EmailInput
,从而达到重置回初始状态的目的嫌
key
杀伤力太大(删除重建,以及组件初始化成本),或者不方便(key
已经有别的作用了)的话,添个props.myKey
结合componentWillReceiveProps
实现局部状态重置
其中,第一种方法只适用于class
形式的组件,后两种则没有这个限制,可根据具体场景灵活选择。第三种方法略绕,具体操作见Alternative 1: Reset uncontrolled component with an ID prop
类似的场景之所以容易出问题,根源在于:
when a derived state value is also updated by setState calls, there isn’t a single source of truth for the data.
一边通过props
计算state
,一边手动setState
更新,此时该state
有两个来源,违背了组件数据的单一源原则
解决这个问题的关键是保证单一数据源,杜绝不必要的拷贝:
For any piece of data, you need to pick a single component that owns it as the source of truth, and avoid duplicating it in other components.
所以有两种方案(砍掉一个数据源即可):
完全去掉
state
,这样就不存在state
与props
的冲突了忽略props change,仅保留第一次传入的props作为默认值
两种方式都保证了单一数据源(前者是props
,后者是state
),这样的组件也可以称之为完全受控组件与完全不受控组件
四.“受控”与“不受控”
组件分为受控组件与不受控组件,同样,数据也可以这样理解
受控组件与不受控组件
针对表单输入控件(<input>
、<textarea>
、<select>
等)提出的概念,语义上的区别在于受控组件的表单数据由React组件来处理(受React组件控制),而不受控组件的表单数据交由DOM机制来处理(不受React组件控制)
受控组件维护一份自己的状态,并根据用户输入更新这份状态:
An input form element whose value is controlled by React is called a controlled component. When a user enters data into a controlled component a change event handler is triggered and your code decides whether the input is valid (by re-rendering with the updated value). If you do not re-render then the form element will remain unchanged.
用户与受控组件交互时,用户输入反馈到UI与否,取决于change
事件对应的处理函数(是否需要改变内部状态,通过rerender
反馈到UI),用户输入受React组件控制,例如:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
// 在这里决定是否把输入反馈到UI
this.setState({value: event.target.value});
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handleChange} />
);
}
}
不受控组件不维护这样的状态,用户输入不受React组件控制:
An uncontrolled component works like form elements do outside of React. When a user inputs data into a form field (an input box, dropdown, etc) the updated information is reflected without React needing to do anything. However, this also means that you can’t force the field to have a certain value.
用户与不受控组件的交互不受React组件控制,输入会立即反馈到UI。例如:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
// input的输入直接反馈到UI,仅在需要时从DOM读取
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
从数据角度看受控与不受控
不受控组件把DOM当作数据源:
An uncontrolled component keeps the source of truth in the DOM.
而受控组件把自身维护的state
当作数据源:
Since the value attribute is set on our form element, the displayed value will always be this.state.value, making the React state the source of truth.
让程序行为可预测的关键在于减少变因,即保证唯一数据源。那么就有数据源唯一的组件,称之为完全受控组件与完全不受控组件
对应到之前派生state的场景,就有了这两种解决方案:
// (数据)完全受控的组件,不再维护输入state,value完全由外部控制
function EmailInput(props) {
return <input onChange={props.onChange} value={props.email} />;
}
// (数据)完全不受控的组件,只维护自己的state,完全不接受来自props的任何更新
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} />;
}
}
P.S.注意,“数据受控的组件”与“受控组件”是完全不同的两个概念,按照受控组件的定义,上例两种都是受控组件
所以,在需要复制props到state的场景,要么考虑把props
收进来完全作为自己的state
,不再受外界影响(使数据受控):
Instead of trying to “mirror” a prop value in state, make the component controlled
要么把自己的state
丢掉,完全放弃对数据的控制:
Remove state from our component entirely.
五.缓存计算结果
另一些时候,拷贝props
到state
是为了缓存计算结果,避免重复计算
例如,常见的列表项按输入关键词筛选的场景:
class Example extends Component {
state = {
filterText: "",
};
static getDerivedStateFromProps(props, state) {
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
// 缓存props结算结果到state
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
能用,但过于复杂了。通过getDerivedStateFromProps
创造了另一个变因(state.filteredList
),这样props change和state change都可能影响筛选结果,容易出问题
事实上,想要避免重复计算的话,并不用缓存一份结果到state
,比如:
class Example extends PureComponent {
state = {
filterText: ""
};
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
const filteredList = this.props.list.filter(
item => item.text.includes(this.state.filterText)
)
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
利用PureComponent
的render()
只在props change或state change时才会再次调用的特性,直接在render()
里放心做计算
看起来很完美,但实际场景的state
和props
一般不会这么单一,如果另一个计算无关的props
或state
更新了也会引发rerender
,产生重复计算
所以干脆抛开“不可靠”的PureComponent
,这样解决:
import memoize from "memoize-one";
class Example extends Component {
state = { filterText: "" };
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
不把计算结果放到state
里,也不避免rerender
,而是缓存到外部,既干净又可靠
文中的完全受控组件与完全不受控组件 和这篇文章中提到的概念正好相反。。。 https://segmentfault.com/a/1190000016376897
现已更正,着重区分“数据受控的组件”与“受控组件”,感谢指出;)
但实际上,componentWillReceiveProps在每次rerender时都会调用,无论props变了没
这句话是错的
这句是没错的。见文中第一处代码示例,以及本文开头引用的官方文档